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

Introduction

# Castellan

Pressure wakes agents — multiplexer, dashboard, and episode flywheel in one binary.

Install
curl -fsSL https://raw.githubusercontent.com/Alphabetsoup16/Castellan/main/install.sh | bash

Castellan is the coordination runtime for multi-agent AI systems. Agents deposit into a typed blackboard and a decaying pheromone field; a pressure scheduler wakes the next agent when dependency signals clear. That is stigmergy — coordination through the environment — and it replaces a chat-router tax with geometry you can log, replay, and evolve.

  • Pressure wake — deterministic scheduling from substrate signals, not a manager model picking the next agent.
  • Mux + dashboard — panes and the operator UI in one binary.
  • Evolve + drift — episodes improve topology; gates keep changes honest.
  • RAH depth-bounded — harness-in-harness recursion with topology binding, not opaque shell-outs.

Episode flywheel

flowchart LR
    RUN[castellan run] --> LOG[.castellan/episodes]
    LOG --> EVOLVE[castellan evolve]
    EVOLVE --> GATE[castellan drift-check]
    GATE --> RUN

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef evolve stroke:#d97706,stroke-width:2px
    classDef gate stroke:#1a7f37,stroke-width:2px

    class RUN,LOG runtime
    class EVOLVE evolve
    class GATE gate
  1. Runcastellan run (add --mux / --rlm when you want panes)
  2. Log — episode JSON under .castellan/episodes/
  3. Evolve — topology mutations from the corpus
  4. Gatecastellan drift-check against the verification manifest

For agents

Paste this into an agent session before it drives Castellan:

Read https://castellan-docs.pages.dev/integrations/agent-guide.html
(or the mirrored agent-guide.md). Operate via multiplexer panes + substrate
tools — not agent-to-agent chat. Prefer castellan herd status and MCP tools.

Full conventions: Agent guide. Lexicon: Conventions.

Install

Get the castellan binary on your machine and confirm the verify trio passes.

Install methods

MethodCommandBest for
Install scriptcurl -fsSL https://raw.githubusercontent.com/Alphabetsoup16/Castellan/main/install.sh | bashQuick try / operators
From clonegit clone … && ./install.shDevelopment
Cargo pathcargo install --path crates/castellan-cliRust contributors
Cargo release buildcargo build --release -p castellan-cliCI / local dev

Requires stable Rust from rustup.rs; toolchain pinned in rust-toolchain.toml.

After install script or release build, ensure castellan is on PATH:

export PATH="$PWD/target/release:$PATH"   # from repo root

Verify trio

Run these three commands after install — all should succeed:

castellan --version
castellan run --goal "reach target" --plugin gridworld
castellan doctor

Episode JSON appears under .castellan/episodes/. For full CI parity, see Verify and ./scripts/verify.sh.

Optional: mdbook (docs contributors)

cargo install mdbook mdbook-mermaid --locked
mdbook build book

Next steps

GoalPage
First product win (mux + dashboard)Quickstart
Day-to-day operator loopDaily driver
Agent onboardingAgent guide
Contributor PR gateVerify before PR

Quickstart

Install Castellan, bring up the multiplexer and dashboard, then land a first episode — under ten minutes when install works.

What you get

Binary on PATH → mux socket → dashboard → first episode JSON.

See it first

Offline / no CDN — static transcript
$ castellan plugin list
$ castellan run --goal "reach target" --plugin gridworld
episode satisfied

Download castellan-demo.cast

1. Install

curl -fsSL https://raw.githubusercontent.com/Alphabetsoup16/Castellan/main/install.sh | bash
castellan --version

From source: Install.

2. Mux + dashboard

castellan multiplexer ensure
castellan dashboard

Defaults:

  • Socket: ~/.config/castellan/castellan.sock
  • Episodes: .castellan/episodes/
  • Config: castellan.toml when present

In another terminal:

castellan herd status

Quit the dashboard with q. Keybindings: Dashboard.

3. First episode through the mux

castellan run --goal "reach target" --plugin gridworld --mux

Episode JSON lands in .castellan/episodes/<goal-id>.json.

When an LLM/editor is wired, swap in a real goal — for example castellan run --goal "…" --plugin shell --mcp. See MCP overview and Agent guide.

Optional: offline smoke (no mux)

castellan run --goal "reach target" --plugin gridworld

Common commands

castellan --help
castellan multiplexer ensure && castellan dashboard
castellan run --goal "reach target" --plugin gridworld --mux
castellan run --goal "reach target" --plugin gridworld --rlm
castellan evolve --episodes 16 --workspace .castellan/episodes
castellan herd status
castellan drift-check
castellan mcp

Next steps

GoalPage
Day-to-day operator loopDaily driver
Dashboard keybindingsDashboard
MCP tool catalogMCP tools
Wire Cursor / editorsCursor setup
Remote / SSH sessionsCLI — remote
Contributor verify before PRVerify
VocabularyConventions

Daily driver

Recommended local workflow for operating Castellan — agents, panes, and the episode flywheel. For contributing to the Rust codebase, see Engineering guide and Verify before PR.

Why this loop

Mux keeps panes alive, the dashboard shows pressure and agents, run writes episodes, evolve mutates topology, drift-check gates what you trust.

Loop

castellan multiplexer ensure  →  castellan dashboard  →  castellan run  →  castellan evolve  →  castellan drift-check
  1. Multiplexercastellan multiplexer ensure starts the in-tree PTY server (~/.config/castellan/castellan.sock). Idempotent; safe at the start of every session. Use --session <name> for an isolated socket.
  2. Dashboardcastellan dashboard opens the ratatui UI (agents, pressure, pane tree). Writes ~/.config/castellan/statusline.json for MCP observability. Keys: j/k select, Enter attach, r refresh, q quit — see Dashboard.
  3. Run — pick a mode:
You wantCommand
Scheduler-led panes (daily default)castellan run --goal "…" --plugin gridworld --mux
Offline / CI smokecastellan run --goal "…" --plugin gridworld
Headless verify/actcastellan run --goal "…" --plugin gridworld --rlm
LLM/editor wiredcastellan run --goal "…" --plugin shell --mcp

Every mode writes .castellan/episodes/<goal-id>.json. Default castellan run is a deterministic plugin loop — add --mcp / --model only when you want LLM enrichment.

  1. Detach / reattach — panes persist without babysitting. Ctrl+Q detaches; later castellan herd tabs / castellan herd attach <pane-or-session>.
  2. Evolvecastellan evolve --episodes 16 --workspace .castellan/episodes. Prefer --plan / --dry-run before applying; MCP exposes castellan_propose_mutation / castellan_resolve_mutation.
  3. Gatecastellan drift-check against the verification manifest before you trust a topology change.

Honest scope: drift-check catches structurally invalid or manifest-violating topology. It does not grade goal quality — use episode verdicts and castellan vitals for that.

Config layers from castellan.toml when present. Episodes and archive live under .castellan/.

Operator commands

castellan multiplexer ensure
castellan dashboard
castellan herd status
castellan run --goal "reach target" --plugin gridworld --mux
castellan evolve --episodes 16 --workspace .castellan/episodes
castellan drift-check

Remote / SSH

castellan remote --ssh user@host --goal "reach target" --plugin gridworld
castellan herd attach --session my-work
castellan herd status

When something breaks

SymptomLikely causeFix
castellan herd status errorsNo mux servercastellan multiplexer ensure
Dashboard opens blankSocket up, no panes yetRun with --mux, then r
Episode missing after runPlugin exited earlyRerun with --json
evolve accepts 0Corpus too smallMore episodes, or --plan first
drift-check rejectsStructural/manifest violationInspect violations; do not force-apply

More symptoms: Troubleshooting. Staged diagnostics: castellan doctor --json.

GoalPage
First-time setupQuickstart
Dashboard keybindingsDashboard
MCP / statuslineMCP overview · Statusline
Agent onboardingAgent guide
Contributor PR gateVerify
LexiconConventions

Troubleshooting

Common first-run failures and fixes. Prefer this page for symptoms; Daily driver for the full operator loop; Conventions for paths and lexicon.

What to check first

  1. castellan --version — binary on PATH
  2. castellan multiplexer ensure then castellan herd status — mux up
  3. castellan doctor — staged diagnostics

castellan: command not found

cargo install --path crates/castellan-cli
# or
cargo build -p castellan-cli && export PATH="$PWD/target/debug:$PATH"

Multiplexer socket missing

castellan multiplexer ensure
# or
castellan herd server

Override socket path: export CASTELLAN_SOCKET=~/.config/castellan/castellan.sock

Episodes / doctor look in the wrong directory

Workspace data defaults resolve via resolve_workspace_data_dir (.castellan/ or legacy .flock/ when present). Override with:

export CASTELLAN_DATA_DIR=/path/to/data

Relative values are resolved against the workspace root (doctor --workspace / process cwd for run/evolve).

drift-check fails locally

./scripts/drift-guard.sh
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings

Guardrail edits require manifest updates — see governance policies.

castellan run exits immediately with plugin error

castellan plugin list
castellan plugin info gridworld

Confirm --plugin name matches registry entry. Shell plugin requires bounded argv — no shell injection.

Dashboard shows disconnected

  1. Start multiplexer: castellan herd server
  2. Attach session: castellan herd attach
  3. Check ~/.config/castellan/statusline.json — see statusline

--json output not parseable

castellan run --json emits one CastellanEvent per line (NDJSON). Do not mix with pretty-printed episode JSON — episode file is written to .castellan/episodes/ separately.

Docs build fails

cargo install mdbook mdbook-mermaid --locked
./scripts/install-mermaid-assets.sh
mdbook build book

What Castellan is / is not

Castellan is

  • A coordination runtime — pressure-field scheduler, typed blackboard, episode logs
  • A plugin harness boundaryCastellanPlugin::verify_goal drives the default castellan run loop
  • An in-tree multiplexer — NDJSON socket API + PTY panes (MUX)
  • A topology evolution loopcastellan evolve mutates wake maps from episodes
  • Governance-awarecastellan.toml hooks, tool pipeline, drift-check manifest

Default castellan run uses deterministic plugins (gridworld, shell). The scheduler wakes agents from substrate signals — not a manager LLM.

Castellan is not

  • A drop-in coding agent with dozens of model providers on the hot path
  • A wrapper around external harness binaries — Castellan is the coordination runtime
  • A meta-harness composition layer that shells out to opaque coding agents as black boxes
  • A workflow DSL — coordination is the pressure field, not hand-drawn graphs
  • A chat router — agent-to-agent NL handoffs are intentionally forbidden

Honest scope: Castellan optimizes multi-agent coordination geometry — scheduler ticks, pheromone deposits, topology evolution, and multiplexer panes. Vocabulary: Conventions. Landscape peers (for maintainers only): Frontier positioning.

Comparison snapshot

NeedCastellanTypical coding agent
Daily coding agentUse castellan acp or editor + MCPPrimary product
Multi-pane agentsIn-tree multiplexer + dashboardExternal tmux scripts
Topology evolutioncastellan evolve + archiveUsually out of scope
Stigmergy / pressure fieldCore schedulerUsually out of scope
Provider matrixOptional verify-path LLM onlyOften hot-path feature

Honest adoption path

NeedUse
Prove coordination thesiscastellan run, swarm-demo, meeting-sched benches in verify.sh
Editor integrationcastellan acp (stdio + prompt → engine)
External tool bridgecastellan mcp or castellan run --mcp
Remote mux panescastellan remote --plugin <name> with governance
Cursor / Claude daily driverPair Castellan multiplexer with agent hooks

Castellan deliberately does not chase coding-agent feature parity — coordination runtime and honest scope are the product.

Agent guide

Outcome: onboard an LLM or editor agent so it drives panes and substrate tools — not agent-to-agent chat.

Onboarding for AI agents operating Castellan panes — Claude Code, Codex, Cursor, and Pi. Paste this page (or llms.txt) into your agent context before driving Castellan.

What you are operating

Castellan is a coordination runtime, not a chat router. Agents coordinate through:

  • Blackboard — typed JSON slots
  • Pheromone field — decaying zone signals that wake the scheduler
  • Multiplexer panes — PTY sessions with NDJSON socket control

Do not coordinate via agent-to-agent natural language. Use substrate tools and pane deposits.

Install (operator machine)

curl -fsSL https://raw.githubusercontent.com/Alphabetsoup16/Castellan/main/install.sh | bash
export PATH="$HOME/.cargo/bin:$PATH"   # if install.sh used cargo install

Verify:

castellan run --goal "reach target" --plugin gridworld
ls .castellan/episodes/

Daily-driver loop

castellan multiplexer ensure          # start socket server
castellan dashboard                   # ratatui dashboard (optional)
castellan herd status                 # agent states across panes
castellan run --goal "..." --plugin shell --mcp

Episode flywheel: castellan run.castellan/episodes/*.jsoncastellan evolvecastellan drift-check.

Agent-specific pane setup

AgentStart in paneHook install
Claude Codeclaude in panecastellan integration install claude
Codexcodex in panecastellan integration install codex
CursorCursor terminal or cursor CLIMCP via castellan mcp (see below)
Pipi in paneUse Pi programmatic/RPC mode in pane

Spawn via socket API:

{"id": 1, "method": "agent.start", "params": {"name": "claude", "focus": true}}

Or CLI: castellan herd attach --pane <id> after castellan herd status.

MCP workflow (Cursor and headless agents)

  1. Operator wires castellan mcp in MCP settings (Cursor setup).
  2. Agent calls castellan_search_toolscastellan_describe_tool → invoke.
  3. For live substrate during episodes, operator runs castellan run --mcp.

Key tools: castellan_deposit_signal, castellan_read_blackboard, castellan_topology_snapshot, castellan_multiplexer_status, castellan_recall, castellan_remember. Always call castellan_discover_tools or castellan_search_tools before invoking a tool you haven’t used yet — do not guess tool names.

Full catalog: MCP tools · MCP overview.

Socket automation (Multiplexer-shaped)

Connect to ~/.config/castellan/castellan.sock. One JSON request per line.

{"id": 1, "method": "ping", "params": {}}
{"id": 2, "method": "agent.list", "params": {}}
{"id": 3, "method": "pane.read", "params": {"pane_id": "w1:p1", "lines": 40}}
{"id": 4, "method": "events.wait", "params": {"match_event": {"agent_status": "idle"}, "timeout_ms": 60000}}

Full method table: Socket API.

Diagnosis recipes

SymptomCheckFix
No socketcastellan herd status failscastellan multiplexer ensure
Agent stuck “working”castellan herd agent explain --pane <id>Wait or pane.report_agent hook
MCP tools missingCursor MCP panelRestart Cursor; verify castellan mcp path
Episode not writtencastellan run exit codeRun with --json; check plugin goal
Drift rejectedcastellan drift-check outputReview castellan.toml governance section
MCP tool call -32601/-32602Wrong tool name or missing required argcastellan_describe_tool for the exact schema — never invent tool names

Flags agents should know

CommandWhen
castellan run --jsonHeadless NDJSON contract (episode_start … episode_end)
castellan run --mcpLive MCP tools on running engine
castellan run --rlmRLM verify/act loop
castellan remote --ssh user@hostRemote mux panes
castellan evolve --episodes NTopology mutation from corpus

Do not invent flags — verify with castellan --help or CLI reference.

Skills

Castellan scans .castellan/skills/ and ~/.cursor/skills/ on castellan run. Author skills as SKILL.md with frontmatter; install via Cursor skills UI or copy into project.

Honest limits

  • Castellan is not OpenCode/Claude Code — no built-in 75-provider coding agent on the hot path.
  • Default castellan run uses deterministic plugins; wire your LLM via MCP or panes.
  • Agent-to-agent chat coordination is forbidden by design.

See What Castellan is / is not.

Cursor setup

Outcome: wire Cursor as an operator — MCP tools for field/mux, pane agents for work.

Use Castellan in Cursor via MCP (castellan mcp). ACP is available for experiments (Zed-class editors) but MCP is the primary Cursor path.

Install

git clone https://github.com/Alphabetsoup16/Castellan.git && cd Castellan
./install.sh
export PATH="$PWD/target/release:$PATH"

Wire MCP in Cursor

Add a server entry to Cursor MCP config (Settings → MCP, or ~/.cursor/mcp.json):

{
  "mcpServers": {
    "castellan": {
      "command": "/path/to/Castellan/target/release/castellan",
      "args": ["mcp"]
    }
  }
}

Set command to your built target/release/castellan path — Cursor spawns this exact binary, so a relative path or an unbuilt release binary will fail silently in the MCP panel. Restart Cursor after editing the config; MCP servers are loaded once at startup.

Discovery flow: castellan_search_toolscastellan_describe_tool → invoke. Full catalog: MCP tools.

First smoke

castellan run --goal "reach target" --plugin gridworld
castellan run --goal "reach target" --plugin gridworld --json
castellan mcp
printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | castellan mcp

Then in Cursor: open the MCP panel (Settings → MCP → castellan) and confirm the server shows connected with tools listed. Ask the agent to call castellan_discover_tools to verify the wire end-to-end.

MCP vs ACP

PathCursor use
castellan mcpPrimary — tool discovery, substrate read/write, same registry as live episodes
castellan acpExperimental — in-process prompt bridge, no MCP tool bridging in Cursor today

Failure paths

SymptomLikely causeFix
Server shows “no tools” in MCP panelBinary not built, or wrong command pathcargo build --release -p castellan-cli; verify path with which castellan or absolute path
Server never connectsCursor cached a stale processRestart Cursor fully (not just reload window)
Tool call returns -32601Tool name typo or stale Cursor cache of tools/listRe-run tools/list; check name against MCP tools
Agent invents a tool nameModel hallucination, not a Castellan bugInstruct the agent to call castellan_discover_tools before guessing
Deposits rejectedGovernance pre_execute hook deniedCheck castellan.toml governance section; see Governance

Skills and memory

Skills: .castellan/skills/ and ~/.cursor/skills/ are both scanned on castellan run.

Memory is the substrate (blackboard, field, episodes) — see Memory architecture. Vector RAG is deferred. Maintainer SQL schema and trait detail: MEMORY_ARCHITECTURE.md.

Agent onboarding

For paste-into-agent context: Agent guide.

MCP overview

Outcome: wire an editor or agent so it can deposit, read the field, and observe the mux — without inventing tool names.

Castellan exposes stigmergy, memory, and harness tools via castellan mcp (stdio JSON-RPC) and live episodes (castellan run --mcp). Both paths share one registry, so tool names, schemas, and governance behavior never drift between editor use and headless runs.

Same tools, two surfaces

castellan mcp
# or during a live episode:
castellan run --goal "…" --plugin shell --mcp

stdio JSON-RPC on stdin/stdout. Methods: initialize, tools/list, tools/call.

Start the server

castellan mcp

Wire your MCP client to stdin/stdout JSON-RPC. Same registry as live episodes — see crates/castellan-mcp/src/registry.rs.

Anatomy: discovery → describe → invoke

flowchart LR
  A[Agent starts] --> B{castellan_discover_tools<br/>or castellan_search_tools}
  B --> C[castellan_describe_tool]
  C --> D[Invoke operational tool]
  D --> E[Governance + trace]

  classDef runtime stroke:#0969da,stroke-width:2px
  classDef substrate stroke:#5b6b8c,stroke-width:2px
  classDef gate stroke:#1a7f37,stroke-width:2px

  class A,B runtime
  class C,D substrate
  class E gate
  1. Discovercastellan_discover_tools returns domain-indexed cards (no full schemas). Optional domain filter.
  2. Searchcastellan_search_tools with query narrows by intent; BM25-ranked, substring fallback for partial words.
  3. Describecastellan_describe_tool with name returns the full schema, preconditions, side effects, and examples.
  4. Invoketools/call (stdio) or the mcp_tools observation payload (live episodes).

Meta-tools always ship full schemas in tools/list; operational tools ship compact cards to keep context small — call castellan_describe_tool before invoking one you haven’t used yet.

Full tool catalog

Verified against LIVE_TOOL_NAMES in crates/castellan-mcp/src/registry.rsdo not invent tool names; if a tool you need isn’t here, it doesn’t exist yet.

Meta tools

ToolPurpose
castellan_discover_toolsBrowse domains and tool names
castellan_search_toolsBM25-ranked keyword search over the catalog
castellan_describe_toolFull schema + preconditions + side effects for one tool

Operational tools

ToolDomainSummaryAnnotations
castellan_deposit_signalsubstrateDeposit pheromone into a zonedestructive, governed pre/post
castellan_read_signalsubstrateRead signal strength from a zoneread-only
castellan_read_blackboardsubstrateRead a blackboard slot by keyread-only
castellan_topology_snapshotsubstrateHarness topology JSON (agents, edges, zones)read-only
castellan_metricssubstrateSession counters (tool calls, deposits, mutations)read-only
castellan_multiplexer_statusmultiplexerObservabilitySnapshot + freshness (stale, age_secs, schema_version)read-only
castellan_propose_mutationevolvePreview a topology mutation (diff + drift); does not applypreview-only
castellan_resolve_mutationevolveAccept or reject a persisted proposaldestructive, high-impact gate
castellan_read_resourcegovernanceResolve a typed URI (episode://, genome://, pane://, blackboard://)read-only, allowlisted schemes
castellan_recallmemoryQuery durable memory (episodes, checkpoint, observations, topology, traces)read-only
castellan_remembermemoryPersist an agent observation to durable memorydestructive
castellan_query_field_historymemoryQuery checkpoint-time pheromone field snapshotsread-only

All 12 operational tools are in LIVE_TOOL_NAMES, so they work identically on castellan run --mcp.

Full schemas, examples, and design rationale: MCP tools reference.

Discovery sequence (copy-paste)

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"castellan_search_tools","arguments":{"query":"deposit","domain":"substrate"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"castellan_describe_tool","arguments":{"name":"castellan_deposit_signal"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"castellan_deposit_signal","arguments":{"zone":"grid","signal":"coordination","amount":2.0}}}
Offline / no CDN — static transcript
$ printf '{"jsonrpc":"2.0","id":1,"method":"initialize"}\n' | castellan mcp
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05",...}}
$ echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"castellan_search_tools","arguments":{"query":"deposit"}}}' | castellan mcp
{"jsonrpc":"2.0","id":2,"result":{"hits":[{"name":"castellan_deposit_signal",...}]}}

Download castellan-mcp-demo.cast

Live field instrument (castellan mcp --attach)

While castellan run is active it publishes a live session under .castellan/live/ (active.json pointer, per-goal snapshot JSON, and an append-only deposit queue). The transport is a file bridge (not a Unix domain socket): the engine drains the queue at tick boundaries—the same semantics a socket would provide, without an async listener inside the sync run loop. External MCP clients hit the same field the scheduler ticks:

castellan mcp --attach auto         # bind to the active live session
castellan mcp --attach <goal_id>    # bind to a specific run
  • castellan_deposit_signal enqueues into the live run; the engine drains the queue each tick, emits CastellanEvent::Deposit, and records the deposit on the episode’s instrument_summary with source: "mcp_attach".
  • castellan_topology_snapshot returns the last published live snapshot (tick, topology, field zones, wake stats).
  • Mutation tools (castellan_propose_mutation, castellan_resolve_mutation) are denied in attach sessions (attach_denied error).
  • If no live session exists, --attach fails at startup (attach_unavailable) instead of silently degrading to a standalone engine. If the run ends while attached, calls fail with attach_lost.

Without --attach, plain castellan mcp still auto-routes deposits to an active live session when one exists (source: "external_mcp"), falling back to its own in-memory engine otherwise. castellan doctor reports the live_session stage.

Episode JSON records all MCP instrument activity:

"instrument_summary": {
  "mcp_tool_calls": 3,
  "mcp_deposits": [
    { "tick": 2, "zone": "grid", "signal": "coordination", "amount": 1.5, "source": "mcp_attach" }
  ],
  "topology_snapshots": [{ "tick": 2, "nodes": 2, "edges": 1 }]
}

Live episode example (castellan run --mcp)

{
  "mcp_tools": [
    {
      "tool": "castellan_deposit_signal",
      "args": { "zone": "grid", "signal": "coordination", "amount": 2.0 }
    }
  ]
}

Put entries under mcp_tools in a plugin’s observation payload; CastellanEngine::call_mcp_tool dispatches them against the same registry and governance pipeline as stdio.

Failure paths

SymptomCauseFix
JSON-RPC -32601Unknown method or tool nameCheck spelling against the table above; run tools/list to confirm
JSON-RPC -32602Invalid params for a toolCall castellan_describe_tool first — required fields are enumerated in inputSchema
castellan_search_tools query rejectedQuery over 256 charactersShorten the query string
Tool call silently no-ops on --mcpTool not in LIVE_TOOL_NAMESAll 12 operational tools currently are — check mcp_tools payload shape matches the JSON above
castellan_propose_mutation never appliesTwo-phase by designCall castellan_resolve_mutation with accept: true and the returned proposal_id
Deposits rejected mid-episodeGovernance pre_execute hook deniedCheck castellan.toml governance section and --audit <PATH> JSONL

Trace JSONL emits mcp_discovery for meta-tools and mcp_tool for operational calls — use --audit on castellan run to capture both.

Editor integration

Statusline and observability

Castellan exposes complementary observability surfaces that share one schema:

  1. statusline.jsonObservabilitySnapshot (file + MCP-readable)
  2. castellan run --json — typed castellan-events NDJSON stream
  3. castellan 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.

WriterWhen
castellan dashboardEvery ~250ms while the TUI is open
castellan statusline refreshOne-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)

FieldMeaning
socketup / down — mux Unix socket ping
eventslive / stale / idle — EventBus activity
panessocket — 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

ConsumerHow
MCPcastellan_multiplexer_status — returns schema_version, written_at, age_secs, stale, plus status body
Headless / CIcastellan statusline refresh --json then jq .schema_version
History DBcastellan 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:

EventWhen
episode_start / episode_endSession boundaries
scheduler_tick / wake_summary / task_dispatchedDispatch
depositPheromone deposit (drives dashboard field heat)
cost_rollupPer-tick usd/tokens + session totals
plan_verify_passed / plan_verify_deniedGuardians prove-before-execute
quorum_fired / zone_quarantine / morphogenesis / genome_expression_appliedBio chips
topology_apply_drainedMid-run topology apply
verify_result / tool_decision / permission_denied / budget_exhaustedGovernance
mutation_proposed / mutation_resolvedEvolve proposals
archive_write_backPost-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.json and MCP for headless observers.

Failure paths

SymptomCauseFix
statusline.json missingNever refreshedcastellan statusline refresh or open castellan dashboard
vitals nullNo episode vitals yetcastellan run --goal "..." --plugin gridworld
MCP stale: trueFile older than ~30sRe-run statusline refresh or keep dashboard open
bus.panes=socket but attach emptyLooking at in-process run panesAttach uses socket tree; ensure panes on mux socket

ACP (Agent Client Protocol)

Castellan ships an ACP v1 stdio agent via castellan acp. Editors such as Zed can spawn Castellan as an external coordination agent — scheduler-primary runs that produce episodes, not chat transcripts.

MCP remains the primary Cursor integration path. See Cursor setup.

Capabilities

MethodStatus
initializeProtocol v1, camelCase agent capabilities
session/newBinds sessionIdcastellan-memory acp_sessions
session/forkFork session with parent lineage (parentSessionId persisted)
session/promptRuns CastellanEngine + persist_episode_bundle; emits session/update stream
session/resumeRestores checkpoint metadata (no chat replay)
session/loadReplays episode metadata via session/update notifications
session/cancelAborts in-flight prompt task
session/closeCancels + marks session closed
permissions/requestGovernance pipeline; returns needs_prompt for Prompt mode (editor UI). Set CASTELLAN_ACP_AUTO_DENY=1 in CI
promptDeprecated alias for session/prompt

Phase 3 (MOAT): mux-by-default when socket healthy; session/fork for parent-child lineage; EpisodeCheckpoint saved on every prompt.

Diagnostics

castellan acp doctor

Prints protocol version, agent capabilities, memory backend, mux health (mux_default_on, mux_effective, mux_socket_healthy), and env flags (CASTELLAN_MUX_AUTO, CASTELLAN_MUX_DISABLE, CASTELLAN_ACP_AUTO_DENY).

Zed smoke (5 steps)

  1. Build Castellan: cargo build --release -p castellan-cli
  2. Copy examples/zed-acp.json into your Zed settings agents list (adjust command path).
  3. Open a workspace with a .castellan/ directory (or let Castellan create one on first prompt).
  4. Start the Castellan agent in Zed and send: reach target with context plugin gridworld.
  5. Confirm the agent returns stopReason: completed, ≥3 session/update notifications, and .castellan/episodes/<goal_id>.json exists.

Mux episodes

Mux is the default when the multiplexer socket is healthy. Explicit "mux": true in prompt context forces mux; "mux": false opts out. Rollback env vars: CASTELLAN_MUX_AUTO=0 or CASTELLAN_MUX_DISABLE=1. ACP attaches pane_tree to persisted episodes for evolve flywheel compatibility.

Session fork

{"jsonrpc":"2.0","id":2,"method":"session/fork","params":{"sessionId":"<parent-id>"}}

Returns { "sessionId": "<child-id>", "parentSessionId": "<parent-id>" }. Child inherits cwd, plugin, and latest goal from parent.

Failure paths

SymptomCauseFix
session/prompt hangsNo .castellan/ dir writable, or plugin missingRun castellan doctor first; confirm --plugin name via castellan plugin list
permissions/request always deniedCASTELLAN_ACP_AUTO_DENY=1 set (CI default)Unset for interactive use, or handle the request explicitly in the editor
Prompted MCP tools denied mid-session/promptEngine path uses headless prompter (fail closed)Editor should approve via permissions/request, or set CASTELLAN_PERMISSION_PROMPT=allow for trusted CI, or mark tools allow in config
session/load returns no messagesBy design — episode metadata only, no chat replaySee Limitations below; use castellan replay --episode <path> for full JSON
Mux not attaching panesSocket unhealthycastellan acp doctor → check mux_socket_healthy; castellan multiplexer ensure

Limitations

  • No full chat transcript replay on session/load — episode metadata only (MEMORY_ARCHITECTURE).
  • External editor MCP servers are bridged for castellan_* tools only in Phase 2; shell/file tools remain editor-side.
  • crates.io publish for library crates is dry-run only until the API stabilizes.

Coordination model

What

Castellan agents coordinate through a shared coordination field, not through conversation. A typed blackboard, decaying pheromone zones, and a pressure-driven scheduler decide who wakes and when — there is no router LLM picking the next agent, no natural-language handoff, and no agent-to-agent chat.

Why

Coordination geometry (topology, zones, wake thresholds) is the thing that gets tuned and evolved, not prompt text. Vocabulary: Conventions.

If you are wiring up a plugin, debugging why an agent never wakes, or trying to understand what castellan run --mux actually does under the hood, this page is the mental model you need.

Note: "Coordination" here means task dependency resolution and signal propagation between concurrent panes/agents in one run — not multi-agent chat orchestration. See What Castellan is / is not.

Substrate primitives

PrimitiveTypeRole
BlackboardTyped JSON slotsStructured read/write state, scoped per plugin manifest ([blackboard] slots = [...])
Pheromone zonesPheromoneFieldNamed signal maps with exponential decay; agents perceive within a radius
Perception radiusScalar per zoneBounds how far an agent reads field signals — no global broadcast
Pressure fieldPressureScheduler queueWakes tasks when dependency signals + base pressure clear a threshold
Completion signalDepositNextAction::Complete deposits a completion signal into the zone; dependents wake
Quorum gate (H1)QuorumSensorPhase unlock when density ≥ Q and ≥ k distinct contributors deposit within window W — colony consensus without a router LLM
Trail corridors (H4/H9)CorridorFieldOptional (from,to,signal) edges with per-edge λ; Physarum conductance c ← c + α·flux − β·c prunes low-flow paths
Cement (H3)cement signalSuccessful verify deposits cement; morphogenesis may birth topology nodes under regulatory caps

Wake pressure is computed as min(base_pressure, min(dependency signals)), optionally boosted by multiplexer pane activity (pane_status_wake_boost). A task with unmet dependencies stays deferred no matter how high its own base pressure is — the field, not an individual agent, decides readiness.

Quorum = colony phase gate. Unlike task wake (deps ∩ pressure), quorum locks a phase transition until multi-contributor density clears — ablate to a single depositor and the phase stays locked (FalseQuorum immune when mass lacks diversity).

Response thresholds (H2). Topology nodes carry private θ maps; among ready tasks the scheduler can prefer the node with highest stimulus−θ surplus — specialization emerges without hardcoded roles.

Allowed vs forbidden coordination patterns

PatternStatusWhy
Deposit completion → dependent wakesAllowedCore substrate mechanism
Blackboard slot handoff (typed JSON)AllowedStructured, auditable, replayable
NextAction::Delegate { task }AllowedExplicit sub-task spawn, still substrate-mediated
Multiplexer pane deposits (pheromone_deposits)AllowedPane activity feeds the pane zone
Agent-to-agent chatForbiddenRe-introduces router-LLM coordination cost and non-determinism
Router LLM choosing next agentForbiddenDefeats the “coordination is geometry, not prompts” thesis
Re-encoding substrate state as natural language for another agent to parseForbiddenLoses structure, adds token cost, adds failure surface

Anatomy: one scheduler tick

flowchart TB
    G[Goal] --> P[encode_task]
    P --> Q[Pending queue]
    Q --> RP[refresh_pressure]
    RP --> DEP{deps met?}
    DEP -->|no| DEFER[deferred]
    DEP -->|yes| DISPATCH[dispatch]
    DISPATCH --> HOST[RunHost]
    HOST --> OBS[observation]
    OBS --> BB[Blackboard]
    OBS --> FIELD[PheromoneField]
    OBS --> V[verify_goal]
    V --> COMP[deposit completion]
    COMP --> FIELD
    FIELD --> RP

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef substrate stroke:#5b6b8c,stroke-width:2px
    classDef gate stroke:#1a7f37,stroke-width:2px

    class G,P,Q,RP,DISPATCH,HOST runtime
    class OBS,BB,FIELD substrate
    class V,COMP gate

CastellanEngine::run_goal walks this loop until the goal is satisfied, exhausted, or a step budget expires:

  1. Encode — the plugin’s encode_task(goal) produces the initial TaskDescriptor.
  2. Decay + refresh — the field decays one tick; refresh_pressure recomputes wake pressure for every pending task.
  3. Dispatch — the ready batch (dependencies met, pressure over threshold) goes to the RunHost (in-process, mux panes, or remote).
  4. Observe — host output updates the blackboard and deposits into zone(s); coordinate_from_observation applies any explicit deposits from the plugin.
  5. Verifyverify_goal checks the observation against the goal; on success it deposits a completion signal so dependents wake next tick.
  6. Snapshot — field state, topology, and wake stats are written into the episode JSON under .castellan/episodes/.

How operators use it

You rarely touch the substrate directly — you drive it through castellan run and read its effects back out of episode JSON and live MCP tools.

1. Run a goal and let the scheduler coordinate:

castellan run --goal "reach target" --plugin gridworld

2. Force scheduler-led multi-pane coordination (topology + pane tree recorded in the episode):

castellan run --goal "reach target" --plugin gridworld --mux

3. Inspect live substrate state while a run is active (via MCP tools, from an editor or castellan mcp client):

castellan_read_blackboard
castellan_deposit_signal

4. Replay a past run to see the coordination trace tick-by-tick:

castellan replay --episode .castellan/episodes/<goal-id>.json

5. Watch multiplexer pane activity feed the field — pane observations can carry pheromone_deposits:

{"pheromone_deposits": [{"zone": "pane", "signal": "activity", "amount": 0.5}]}

These deposits feed pane_status_wake_boost, which nudges wake pressure for stalled tasks whenever panes report activity — useful when a long-running shell pane is making progress but hasn’t hit a dependency signal yet.

Recipes

When a plugin has independent sub-tasks that must merge before completion, use NextAction::Delegate to spawn them and let the blackboard carry results back — don’t have the parent poll or message the children in natural language.

When you want a run to react to external pane activity (e.g. a human working in another tab), enable --mux so pane deposits land in the pane zone and boost wake pressure for dependent tasks.

When debugging “my task never wakes,” dump the episode JSON and check pending/deferred tasks against their declared dependencies — the field will not dispatch a task early no matter how urgent it looks to a human reader.

When you need deterministic, replayable coordination for CI, avoid --rlm/LLM-driven verify and stick to plugin-native verify_goal — the scheduler loop itself never calls an LLM.

Failure paths / troubleshooting

SymptomLikely causeFix
Task stuck in deferred foreverDependency never deposits completion (upstream task failed or plugin didn’t verify)Check upstream task’s verify_goal result in episode JSON; fix the plugin’s completion condition
Pane activity doesn’t seem to speed anything up--mux not set, so pane deposits never reach the scheduler’s fieldRe-run with --mux, or confirm the multiplexer socket is up (castellan multiplexer ensure)
Two plugins fight over the same zoneZones overlap in plugin.toml [zones] without intentGive each plugin a distinct primary zone; share default zones deliberately
Wake pressure looks “stuck” below thresholdPerception radius too small to see the depositing zoneWiden the zone’s perception radius or deposit into a zone the waiting task actually watches
Coordination trace missing from episodeRun used --no-mux / in-process host without topology trackingRe-run with --mux if you need pane_tree and per-zone deposit history

See also

Organism model

What

Castellan treats a harness as a living organism, not a static config file: a genome expresses coordination parameters, circulation vitals track field health tick-by-tick, and an immune gate rejects harmful topology mutations before they reach production.

Why

This layer turns “evolve topology” from a black box into something you can inspect, quarantine, and trust. Prefer organism terms over animal-pack or feudal metaphors — lexicon.

Read this page if you’re trying to understand what castellan vitals is actually showing you, why a mutation got rejected instead of accepted, or how genome lineage relates to the topology archive.

Status: Phases 1–4 are complete — circulation, DNA lineage, immune gate, and per-zone expression are all live in castellan-evolve/castellan-runtime.

Anatomy: where organism state lives

LocationHolds
castellan-core/organism.rsDNA types, circulation vitals, per-zone expression
castellan-substrate/pheromonePer-zone decay (falls back to a global lambda if unset)
castellan-runtime/circulationVitals computation + immune_fever tracking
castellan-runtime/engine.rsWires vitals, genome_id, fever, and decay into the run loop
castellan-evolve/genome.rsLineage, crossover, archive reconstruction
castellan-evolve/immune.rsThreat detection (ThreatPattern), auto_reject, quarantine recommendation
castellan-evolve/mutator.rsCrossover + cold-zone decay adjustment
castellan-cli/organism_cmd.rscastellan vitals, castellan genome list|lineage|map

Bio-novelty extensions (H1–H9)

MechanismSeam
Quorum phase locksQuorumSensor + FalseQuorum immune
Response thresholdsAgentNode.response_threshold + surplus dispatch
Cement morphogenesiscement deposits → MorphogenesisEngine
Trail / Physarum corridorsCorridorField
MAP-Elites archiveMapElitesArchive + castellan genome map
GRN expressionRegulatoryNetwork::express(vitals)
Live quarantineZoneQuarantine mid-run
Clonal immune memory.castellan/immune_memory.json

Circulation vitals also expose role_entropy, role_stickiness, inflammation, and mean_corridor_length when those mechanisms fire.

Organism loop

flowchart LR
    RUN[castellan run] --> VITALS[circulation vitals]
    VITALS --> EP[episode JSON]
    EP --> EV[castellan evolve]
    EV --> IMM[immune_report]
    IMM -->|accept| ARCH[genome archive]
    IMM -->|reject| QUAR[quarantine]

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef evolve stroke:#d97706,stroke-width:2px
    classDef gate stroke:#1a7f37,stroke-width:2px

    class RUN,VITALS,EP runtime
    class EV,ARCH evolve
    class IMM,QUAR gate
  1. Every run expresses a CastellanGenome — the coordination and decay parameters active for that episode.
  2. The PheromoneField’s circulation health lands in the episode as CirculationVitals.
  3. castellan evolve mutates or crosses over (requires a Pareto front with ≥ 2 non-quarantined entries) candidate genomes, then gates every candidate through immune_report().
  4. Quarantined genomes are excluded from archive.nearest() — they can never be selected as a seed for a future castellan run --seed-archive.
  5. Per-zone decay gets tuned from the proposer’s cold-zone heatmap, so zones that go quiet decay differently from zones under constant pressure.

The immune gate, concretely

ImmuneReport carries two independent signals per candidate:

FieldMeaningEffect
auto_rejectSet when threats include RunawayParallel or other severe patternsCandidate never enters the archive this generation
quarantine_recommendedSet whenever any threat pattern is detectedCandidate is marked quarantined: true; excluded from future nearest() seeding even if accepted

A genome can be accepted into the archive but still quarantined — quarantine is about seeding safety, not archive membership. Check castellan genome list output for the quarantined flag per entry.

How operators use it

1. Run a goal — a genome is expressed automatically, no flags needed:

castellan run --goal "reach target" --plugin gridworld

2. Inspect circulation health for the most recent (or a specific) episode:

castellan vitals
castellan vitals --episode .castellan/episodes/<goal-id>.json

3. Evolve genomes from the episode corpus — this is where the immune gate runs:

castellan evolve --episodes 10 --workspace .castellan/episodes

4. List genomes and trace lineage:

castellan genome list
castellan genome lineage <genome_id>

5. Seed a new run from a surviving (non-quarantined) genome:

castellan run --goal "reach target" --plugin gridworld --seed-archive --write-back

Phase completion

PhaseDeliverableStatus
1 Circulationcirculation_vitals, castellan vitals, fitness dimension
2 DNAgenome_id lineage, archive seed reconstruction, castellan genome
3 ImmuneDrift + fitness + immune gate, quarantine, fever deposit
4 ExpressionPer-zone decay, crossover, flux-grid test, fever unit test

flux-grid (optional, experimental)

  • Feature flag: castellan-substrate/flux-grid — a vendored 64×64 Stigmergy grid.
  • Not enabled in default builds. The zone-keyed PheromoneField remains the default substrate.
  • Enable for experiments:
cargo build -p castellan-substrate --features flux-grid

Recipes

When a run’s fitness looks fine but the field “feels” unhealthy, check castellan vitals before trusting the number — circulation vitals surface stalls and dead zones that a single scalar fitness score can hide.

When castellan evolve keeps rejecting your best-looking candidates, dump immune_report reasoning via --plan (read-only, zero writes) before assuming the mutator is broken — the drift or immune gate may be catching a real structural problem.

When you want to know if a genome is safe to seed from, always check quarantined on castellan genome list, not just whether it’s present in the archive — presence and safety are separate signals.

When investigating a lineage regression, castellan genome lineage <id> walks parent_genome_id; note that crossover sets a full parent_ids list internally, but the CLI currently surfaces only the primary parent (see Known deferred below).

Failure paths / troubleshooting

SymptomLikely causeFix
castellan vitals reports no dataNo episodes yet, or wrong --workspaceRun castellan run first; confirm .castellan/episodes/ has files
Genome accepted but castellan run --seed-archive never picks itGenome is quarantined: trueCheck castellan genome list; quarantined entries are excluded from nearest()
Crossover never happensPareto front has fewer than 2 non-quarantined entriesRun more episodes/generations to build a larger accepted pool
Lineage shows only one parent for a crossover genomeCLI limitation — parent_ids is tracked internally but only parent_genome_id is surfacedKnown deferred; inspect the archive JSON directly for full parent_ids if needed
flux-grid feature won’t buildNot enabled in default feature setBuild with cargo build -p castellan-substrate --features flux-grid explicitly

Known deferred

  • flux-grid is not wired into the swarm-demo default path.
  • Full parent_ids lineage is not yet exposed by the CLI (only primary parent_genome_id).
  • RAH_MAX_DEPTH cycle avoidance between castellan-evolve and castellan-rah lives in castellan-core but isn’t documented end-to-end yet.

See also

Topology evolution

What

Evolution that changes coordination geometry — wake thresholds, topology nodes, per-zone decay — not prompt strings. castellan evolve reads real episode logs, proposes structural mutations, gates them through drift + immune checks, and keeps only genomes that measurably improve plugin-grounded fitness.

Why

Outcome: improve harness topology from episode logs under governance gates — fitness from geometry you can replay, not from rewriting prompts. Round-trip seeding: Evolve round-trip. Lexicon: Conventions.

If you’re trying to prove the evolve loop works, debug why a generation got rejected, or wire evolve into CI, this page walks the pipeline end to end.

What this proves

  1. Episode logs from real castellan run / swarm-demo runs ground fitness via plugin_grounded_fitness() — not a synthetic benchmark.
  2. castellan evolve --episodes 10 runs accept/reject over generations, gated by drift, convergence, and immune gates.
  3. Drift gate: structurally incompatible candidates (e.g. edges referencing missing nodes) are rejected on the production path; drift_rejected and drift_violations appear in CLI JSON output when the gate fires.
  4. Convergence gate (potential-game): candidates with zero decay, unreachable wake thresholds, or disconnected stigmergy topology are rejected outright, and any candidate whose potential Φ drops below the incumbent’s is rejected even when fitness improves (convergence:potential_decline); convergence_rejected and convergence_violations appear in CLI JSON.
  5. Gen-0 vs gen-N fitness delta and full genome lineage are observable directly in CLI output — nothing is hidden behind an opaque score.
  6. MAP-Elites (H5): behavior descriptors illuminate a grid of viable geometries (castellan genome map); multi-objective FitnessVector keeps success + circulation per cell.
  7. Clonal immune memory (H8): failure antigens clone detectors into .castellan/immune_memory.json so similar failures quarantine faster than static enums alone.

Anatomy: the evolve pipeline

flowchart LR
    EP[episode corpus] --> PROP[proposer]
    PROP --> MUT[mutation candidate]
    MUT --> DRIFT{drift gate}
    DRIFT -->|fail| REJ1[rejected: drift_violations]
    DRIFT -->|pass| CONV{convergence gate}
    CONV -->|fail| REJ1B[rejected: convergence_violations]
    CONV -->|pass| IMM{immune gate}
    IMM -->|auto_reject| REJ2[rejected: quarantine candidate]
    IMM -->|pass| FIT[plugin_grounded_fitness]
    FIT --> ARCH[topology archive]

    classDef evolve stroke:#d97706,stroke-width:2px
    classDef gate stroke:#1a7f37,stroke-width:2px

    class EP,PROP,MUT,FIT,ARCH evolve
    class DRIFT,IMM gate
StageQuestion it answersWhere it lives
ProposerWhat mutation should we try?castellan-evolve/proposer.rs — reads pheromone heatmaps + wake counts; field-shaped birth/death targets cold zones (role: zone:{name}); live MCP instrument deposits (instrument_summary / .castellan/live/) bias birth toward instrumented zones (instrument_informed on evolve JSON)
Drift gateIs the candidate structurally valid?evolution_drift_violations before fitness
Convergence gateDoes the candidate satisfy potential-game assumptions (decay > 0, viable wake, stigmergy connectivity) and keep the potential function Φ non-decreasing (coverage + decay band − deferred pressure − entropy − wake mismatch)?evaluate_convergence / coordination_potential in castellan-evolve/convergence.rs
Immune gateDoes the candidate exhibit a known threat pattern?castellan-evolve/immune.rsimmune_report()
FitnessDid it actually work better?plugin_grounded_fitness() against real episodes
ArchiveShould we keep it, and is it safe to seed from later?castellan-evolve/archive.rsTopologyArchive

How operators use it

1. Build the CLI and run the scripted proof end to end:

cargo build --release -p castellan-cli
bash docs/demo/evolve-proof.sh

2. Or step through it manually — generate real episodes, then evolve from them:

castellan swarm-demo
castellan run --goal "reach target" --plugin gridworld   # repeat 3x for a small corpus
castellan evolve --episodes 10 --workspace .castellan/episodes
castellan genome list
castellan genome lineage <genome_id>

3. Do a read-only analysis pass before committing to a mutation:

castellan evolve --plan

--plan emits an evolution plan JSON (incumbent, proposed coordination, pressure summary, drift preview, fitness estimate) with zero writes — safe to run against a production archive.

4. Two-phase gated apply when you want a human or CI check between propose and apply:

castellan evolve --dry-run                      # writes .castellan/proposals/<uuid>.json, no archive writes
castellan evolve --apply-proposal <uuid>        # re-checks drift gate, then applies

5. Live pane topology apply — after field-shaped mutations, apply the seed→target delta to an in-process multiplexer pane tree (typed birth/prune, not chat routing):

castellan evolve --episodes 3 --workspace .castellan/episodes --apply-live

JSON includes live_apply (born, pruned, pane_count). When gates reject the incumbent update, --apply-live still trials the last field-shaped candidate onto live panes and writes .castellan/live/<goal>.topology_apply.jsonl (MCP attach remains instrument-only).

Mid-run drain: while a live-instrumented castellan run is active, the engine drains that JSONL on scheduler ticks (latest-wins), updates topology + wake threshold, emits topology_apply_drained / CastellanEvent::TopologyApplyDrained, and on --mux reconciles panes via RAH apply — without a separate --apply-live pass.

6. Wire the same two-phase flow through MCP (castellan_propose_mutation / castellan_resolve_mutation) when driving evolve from an editor agent instead of the CLI.

Sample output shape

{
  "generations": 10,
  "accepted": 4,
  "rejected": 6,
  "drift_rejected": 1,
  "drift_violations": [
    { "check_id": "topology_edge_refs", "message": "edge agent-1→agent-2 references missing node(s)" }
  ],
  "gen0_fitness": 0.69,
  "final_fitness": 0.74,
  "fitness_delta": 0.05,
  "field_mutations": ["birth_cold_zone", "tune_wake"],
  "instrument_informed": true,
  "seed_source": "episodes",
  "pareto_front": []
}
FieldMeaning
accepted / rejectedCount of genomes that survived vs. were dropped this evolve call
drift_rejectedSubset of rejected that failed the structural drift gate specifically
drift_violationsOne entry per structural failure, with a stable check_id
gen0_fitness / final_fitness / fitness_deltaPlugin-grounded fitness trend across generations
field_mutationsField-shaped actions applied (birth_cold_zone, tune_wake, …)
instrument_informedTrue when live MCP instrument deposits biased those field mutations
seed_sourceWhere the initial population came from (episodes here)
pareto_frontNon-dominated candidates eligible for crossover next round

Recipes

When you want a fast, LLM-free CI smoke test of the evolve loop, run the fixture-backed proof rather than a live corpus:

cargo run -q -p evolve_proof

This uses tests/fixtures/evolve-episodes/ (4 gridworld episodes) — no network, no LLM, deterministic.

When a generation rejects everything, check drift_violations first — a structural issue (missing node reference, invalid zone) will reject before fitness is even computed, so a 100% rejection rate often means one bad mutation shape, not a fitness problem.

When you want continuous improvement without babysitting every generation, use --seed-archive + --write-back on production runs (see Evolve round-trip) so successful runs feed fitness back automatically.

When reviewing a mutation before it goes live, prefer --plan--dry-run--apply-proposal over a bare castellan evolve, so you get an inspection point between proposal and archive write.

Honest limits

Honest scope: Fitness is plugin-grounded from in-tree gridworld episodes, not the upstream meeting-sched LLM reference band (48.5%) or any external leaderboard. Mutations target wake threshold, topology nodes, and per-zone decay — not prompt strings. Crossover only activates once the Pareto front has at least 2 non-quarantined entries. The drift gate blocks structurally invalid or manifest-failing candidates before fitness is ever computed.

Failure paths / troubleshooting

SymptomLikely causeFix
castellan evolve exits with “no episodes found”Wrong --workspace, or no runs have completed yetRun castellan run a few times first; pass --workspace .castellan/episodes explicitly
100% rejection rateDrift gate catching a structural issue in every candidateInspect drift_violations[].check_id; fix the proposer input or plugin manifest referenced
Crossover never triggersPareto front has fewer than 2 non-quarantined survivorsRun more generations/episodes to build a larger accepted pool
Fitness delta looks suspiciously highSmall/unrepresentative episode corpusGrow the corpus with real castellan run episodes before trusting the delta
--apply-proposal fails after a successful --dry-runProposal re-checked against drift gate and failed on apply (archive changed in between)Re-run --dry-run to regenerate a fresh proposal against current archive state

Fast-slow co-evolution

Castellan splits fast per-episode RLM remediation from slow topology evolution — two timescales on the same episode corpus, not a monolithic agent chat loop.

LoopSpeedMechanismConfig
FastEvery episodeFailed episodes → fast_remediation observations + pheromone hints[coevolution] fast_remediation = true
SlowEvery N episodesIn-tree castellan evolve on episode corpus[coevolution] slow_evolve_every = 4
[coevolution]
fast_remediation = true
slow_evolve_every = 4
slow_evolve_generations = 2

Demo:

bash docs/demo/fast-slow-coevolution.sh

State is tracked in .castellan/flywheel_state.json; slow evolve emits a slow_evolve event in episode JSON when it runs.

Evolve → run round-trip

What

Loop: episodes → evolve → archive → seed run → write-back fitness.

Why

It’s not enough for castellan evolve to find a better genome once — the round-trip closes so a production run can seed from that genome, and its outcome feeds the archive for the next evolve pass. See Topology evolution and Organism model.

Overview

Castellan closes the loop between topology evolution and production runs in three steps:

  1. castellan evolve — mutates harness topology from the episode corpus; writes .castellan/topology_archive.json.
  2. castellan run --seed-archive — seeds topology/coordination from the nearest archive entry (matched by plugin + goal environment_fingerprint); logs a genome_id and a genome_seed event in the episode.
  3. Write-back — after a successful run, merges the new episode’s fitness into the matching archive entry (matched by genome_id), then re-sorts the archive by success score.

Write-back is enabled automatically whenever --seed-archive is used, or explicitly with --write-back. Pass --no-write-back to disable the automatic path (useful when you want to seed from a known-good genome without letting a noisy or exploratory run pollute its fitness history).

Anatomy: what’s in an archive entry

Each TopologyEntry in .castellan/topology_archive.json carries:

FieldMeaning
idArchive entry UUID
coordinationHarnessCoordination — the topology + wake/decay parameters this entry expresses
fitnessFitnessVector — plugin-grounded scores, updated by write-back
generationWhich evolve generation produced this entry
environment_fingerprintPlugin + goal signature used by nearest() to match seed candidates
genome_id / parent_genome_idLineage — see Organism model
quarantinedExcludes this entry from nearest() regardless of fitness — see Organism model
decay_lambda, zone_decay, zone_weightsPer-zone tuning this genome expresses
flowchart LR
    EP[episode corpus] --> EV[castellan evolve]
    EV --> ARCH[topology_archive.json]
    ARCH --> SEED[castellan run --seed-archive]
    SEED --> RUN[episode: genome_seed event]
    RUN -->|Satisfied + fitness| WB[write_back_fitness]
    WB --> ARCH

    classDef evolve stroke:#d97706,stroke-width:2px
    classDef runtime stroke:#0969da,stroke-width:2px

    class EP,EV,ARCH,WB evolve
    class SEED,RUN runtime

How operators use it

1. Quick reproduce from a fixture corpus:

EVOLVE_FIX="tests/fixtures/evolve-episodes"
mkdir -p .castellan/episodes
cp "$EVOLVE_FIX"/*.json .castellan/episodes/

castellan evolve --episodes 10 --workspace .castellan/episodes
castellan run --goal "reach target" --plugin gridworld --seed-archive --json

Expect in the resulting episode JSON:

  • genome_id — seeded from the nearest archive entry
  • fitness — plugin-grounded success score for this run
  • events[] containing a genome_seed event, and — when write-back applies — an archive_write_back event

2. Seed from a specific genome instead of “nearest”:

castellan run --goal "reach target" --plugin gridworld --genome <genome_id>

--genome and --seed-archive are mutually exclusive — pick one.

3. Seed without letting the run mutate the archive (e.g. reproducing a past result exactly):

castellan run --goal "reach target" --plugin gridworld --genome <genome_id> --no-write-back

4. Point at a non-default archive path (e.g. testing a candidate archive before promoting it):

castellan run --goal "reach target" --plugin gridworld --seed-archive --archive .castellan/topology_archive.candidate.json

Write-back semantics

  • Trigger: GoalStatus::Satisfied and both genome_id and fitness present on the episode — a failed or inconclusive run never writes back.
  • Merge: TopologyArchive::write_back_fitness updates the matching entry by id, then re-sorts the archive by success score so future nearest() calls prefer the freshest evidence.
  • Archive path: controlled by --archive (default .castellan/topology_archive.json).

JSON events

With --json, archive write-back emits a typed archive_write_back CastellanEvent line, immediately before episode_end. Automation watching the NDJSON stream can key off this event to know the archive changed without re-reading the file.

Recipes

When proving the round-trip works end to end (e.g. for a demo or CI check), use the fixture corpus reproduction above — it’s deterministic and doesn’t depend on a live LLM or long-running episodes.

When you want continuous improvement in a daily-driver loop, always pass --seed-archive on production runs (write-back is then automatic) so every successful run compounds into the next evolve generation.

When comparing “does seeding actually help,” run the same goal once with --seed-archive and once without, then diff fitness in the two episode JSONs — the delta is the seeding effect for that goal/plugin pair.

When you need a stable baseline for regression testing, seed with an explicit --genome <id> and --no-write-back so the archive never drifts between test runs.

Failure paths / troubleshooting

SymptomLikely causeFix
--seed-archive seeds nothing (no genome_seed event)No archive entry matches the plugin+goal environment_fingerprint, or all matching entries are quarantinedRun castellan evolve first to populate the archive; check castellan genome list for non-quarantined entries
error: specify only one of --genome or --seed-archiveBoth flags passed togetherPick one — explicit genome or nearest-match auto-seed
No archive_write_back event even though the run succeededEpisode’s fitness or genome_id missing, or --no-write-back was setConfirm the plugin’s verify_goal sets GoalStatus::Satisfied; drop --no-write-back if present
Write-back seems to “lose” a good fitness scoreA later, worse run wrote back to the same genome_id and got re-sorted lowerUse --no-write-back when reproducing/benchmarking a known-good genome so exploratory runs can’t overwrite it
--archive <path> run can’t find the filePath doesn’t exist yet — archive is created by castellan evolve, not by runRun castellan evolve --workspace .castellan/episodes once to create the archive file first

Memory architecture

What

Castellan memory is the coordination substrate plus durable episode artifacts — it is not a vector store, not a conversation archive, and not RAG. There is no embedding index in the default path; “recall” means querying structured episode/checkpoint/observation records, and “remember” means persisting a typed observation, not chunking text for similarity search.

Why

Hot field state (blackboard + pheromones) must stay fast and ephemeral; warm episodes must remain replayable for evolve. Lexicon: Conventions.

If you’re deciding whether to enable SQLite, wiring castellan_recall/castellan_remember from an MCP client, or figuring out where a run’s state actually lives on disk, this page is the map.

Note: Compared to vector-RAG systems, Castellan memory is environment-mediated — typed slots and decaying zone signals, not embedding similarity. See Coordination model.

Anatomy: hot / warm / cold layers

LayerWhat lives hereWhereLifetime
HotBlackboard, PheromoneField, scheduler queueIn-process memoryOne run
WarmEpisodes, traces, checkpoints, topology archive.castellan/ — JSON by default, or SQLiteDurable, local disk
ColdTurso Sync (libSQL replica)Multi-machine shareDurable, remote — off by default, opt-in
flowchart TB
    subgraph hot["Hot"]
        BB[Blackboard]
        FIELD[PheromoneField]
        SCHED[Scheduler]
    end

    subgraph warm["Warm"]
        EP[episodes]
        TR[traces]
        TA[topology archive]
    end

    RUN[castellan run] --> SCHED
    SCHED --> FIELD
    RUN --> BB
    RUN --> EP
    RUN --> TR
    EVOLVE[castellan evolve] --> EP
    EVOLVE --> TA
    MCP[castellan mcp] --> warm
    REPLAY[castellan replay] --> EP

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef substrate stroke:#5b6b8c,stroke-width:2px
    classDef evolve stroke:#d97706,stroke-width:2px

    class RUN,REPLAY runtime
    class BB,FIELD,SCHED,MCP substrate
    class EVOLVE,EP,TR,TA evolve

On-disk layout

ArtifactPathWritten by
Episodes.castellan/episodes/<goal-id>.jsoncastellan run (episode end)
Traces.castellan/traces/<goal-id>.jsonlcastellan run (per-tick)
Checkpoints.castellan/checkpoints/castellan run (for --resume)
Topology archive.castellan/topology_archive.jsoncastellan evolve, and castellan run --write-back
Proposals.castellan/proposals/castellan evolve --dry-run
SQLite DB (optional).castellan/castellan.db (configurable via [memory] path)castellan run/evolve when backend = "sqlite"

How operators use it

1. Default path — nothing to configure. Every run writes episode JSON automatically:

castellan run --goal "reach target" --plugin gridworld
castellan replay --episode .castellan/episodes/<id>.json
castellan memory status

2. Read live substrate state from an editor/MCP client while a run is active:

castellan_read_blackboard
castellan_deposit_signal

3. Switch to the SQLite backend when you want queryable aggregates instead of scanning JSON files. Add to castellan.toml:

[memory]
backend = "sqlite"
path = ".castellan/castellan.db"
mirror_json = true
auto_import = true

mirror_json (default true) keeps writing .castellan/episodes/*.json alongside SQL so tooling that reads plain JSON keeps working. auto_import (default true) imports any existing JSON corpus the first time SQLite opens.

4. Query durable memory from MCP tools once SQLite is enabled:

castellan_recall
castellan_remember
castellan_query_field_history

5. Confirm which backend is active and where the DB lives:

castellan memory status

6. Mid-episode durable checkpoints: persist scheduler state during a run so castellan run --resume survives process loss — not just episode-end snapshots.

[memory]
checkpoint_interval_ticks = 10   # 0 = episode-end only (default, backward-compatible)

Checkpoints land in .castellan/checkpoints/ and SQLite field_history when backend = "sqlite". The engine fires on_checkpoint every N ticks; CLI wires this to MemoryStore::save_checkpoint_json. Integration tests: durable_checkpoint.rs, durable_resume.rs.

Recipes

When you’re prototyping a plugin, stay on the JSON default — episodes are human-readable, diffable, and git-friendly for fixture corpora (see tests/fixtures/evolve-episodes/).

When your episode corpus grows past a few hundred runs and castellan evolve gets slow, switch to backend = "sqlite" so evolve reads SQL aggregates instead of re-parsing every JSON file.

When you need memory to survive across machines (e.g. a shared team archive), configure [memory.sync] with a Turso/libSQL URL and set sync_on_episode_end = true — tokens are referenced by env var name (turso_token_env), never stored inline in castellan.toml. On each castellan run / MCP session open, Castellan pulls newer remote episodes into the local SQLite file; after each episode ends it pushes that episode’s rows when sync_on_episode_end = true.

Check sync health:

castellan doctor --json | jq '.stages[] | select(.name=="memory_sync")'

The memory_sync stage reports reachability, last pull/push timestamps, and the last error (if any). State is stored in .castellan/sync_state.json (no tokens).

When you want a clean corpus for a benchmark run, cp a known-good fixture set into .castellan/episodes/ before castellan evolve rather than hand-editing the live archive.

What never goes in the database

DataWhy
Live pheromone ticksIn-process decay semantics only make sense mid-run; snapshots go in, not the tick stream
Conversation transcriptsAnti-coordination design — see Coordination model
Vector embeddingsOut of scope for v1 — no similarity search in the default path

Failure paths / troubleshooting

SymptomLikely causeFix
castellan evolve says “no episodes found”Wrong --workspace, or episodes never written (run failed before episode_end)Confirm .castellan/episodes/ has files; pass --workspace .castellan/episodes explicitly
SQLite backend configured but episodes still look emptyauto_import ran before any JSON existed, or path points to a fresh DBRe-run castellan doctor --json to confirm the resolved DB path; check mirror_json output
castellan_recall returns nothingBackend is still json (default) — castellan_recall is a SQLite-backed toolSet [memory] backend = "sqlite" in castellan.toml
Turso sync silently not happeningsync.turso_url unset, sync_on_episode_end false, or [memory] backend not sqliteSet turso_url, sync_on_episode_end = true, backend = "sqlite"; export token env from turso_token_env; castellan doctor memory_sync stage shows last error
Two commands disagree on episode countOne reads JSON, one reads SQLite, and mirror_json was toggled mid-projectKeep mirror_json = true, or fully migrate and stop reading the JSON tree

Engineering detail

Full SQL schema, the MemoryStore trait, and sync policy internals: MEMORY_ARCHITECTURE.md (maintainer spec, not a book page).

See also

RLM loop

In-tree verify/act loop: castellan run --rlm.

Sandbox host (RlmSandboxHost)

Hardened process-isolated code eval ships behind the sandbox feature on castellan-rlm (enabled by default on castellan-cli via rlm-sandbox).

BackendTypeWhen
StubSandboxHostDefault without sandbox featureReturns stub message
ProcessSandboxHostpython3 -c subprocess + timeoutsandbox_backend = "process"
HardenedProcessSandboxHostTemp workspace, no network, platform hardeningDefault (sandbox_backend = "hardened")
BubblewrapSandboxHostbwrap on PATH with ro-bind + unshare-netsandbox_backend = "bubblewrap" (falls back to hardened)
WasmSandboxHostwasmtime echo guest (minimal eval scope)sandbox_backend = "wasm" with --features wasm-sandbox (experimental; not default)

Configure via CASTELLAN_SANDBOX_BACKEND=process|hardened|bubblewrap or [rlm] sandbox_backend in castellan.toml. WASM requires the optional wasm-sandbox feature — not enabled by default.

Hardened process remains the default hot path. WASM ships a minimal echo guest for REPL eval wiring only — not Python parity.

REPL eval path

When an observation includes an rlm_eval block, the RLM loop executes code through the sandbox host before verify:

{
  "rlm_eval": { "code": "print(2 + 2)", "timeout_ms": 5000 },
  "rlm_eval_result": { "stdout": "4\n", "stderr": "", "exit_code": 0 }
}

Wiring:

  • castellan-rlm: repl_eval_observation, run_until_satisfied(..., sandbox_host), select_sandbox_host()
  • castellan run --rlm: mux RLM path uses select_sandbox_host() (hardened default)

Build / test

cargo test -p castellan-rlm --features sandbox
CASTELLAN_SANDBOX_BACKEND=bubblewrap cargo test -p castellan-rlm --features sandbox
CASTELLAN_SANDBOX_BACKEND=wasm cargo test -p castellan-rlm --features sandbox,wasm-sandbox
cargo test -p castellan-cli rlm

Multiplexer overview

Castellan ships an in-tree PTY multiplexer with a Multiplexer-shaped NDJSON socket API. One binary (castellan) serves daily-driver mux panes — workspaces, tabs, splits, agent detection — without requiring a separate external mux client binary. It is both a terminal manager you can drive by hand (castellan herd attach) and a coordination substrate the scheduler can drive automatically (castellan run --mux).

If you’re setting up a daily-driver pane layout, wiring an editor agent to report its own state, or trying to understand what --mux actually spawns, this page is the map.

Anatomy

castellan herd server  →  castellan-multiplexer (Unix socket)
        │
        ├── workspace / tab / pane tree      (session.snapshot, workspace.*, tab.*, pane.*)
        ├── pane.spawn / pane.attach          (PTY proxy, streaming)
        ├── events.subscribe / events.emit    (NDJSON EventBus — live Castellan events when socket healthy)
        └── pane.report_agent                 (hook protocol — authoritative agent state)
ConceptWhat it is
ServerLong-lived process owning the Unix socket and the pane tree; started by castellan herd server or castellan multiplexer server
WorkspaceTop-level grouping of tabs (roughly: one project or one logical session)
TabA BSP-splittable container of panes
PaneOne PTY — either a plain shell, or a pane running an agent (Claude/Codex/Cursor/etc.)
SessionA named server instance with its own socket + persist dir — lets you run multiple independent multiplexer servers

Pane agent status feeds the coordination substrate directly: agent_status observations can carry pheromone_deposits, which boost scheduler wake pressure on the pane zone. See Coordination model for the mechanism.

flowchart LR
    SRV[castellan herd server] --> SOCK[Unix socket]
    SOCK --> PANE[panes]
    PANE --> AGENT[agent_status]
    AGENT --> DEPOSIT[pheromone_deposits]
    DEPOSIT --> FIELD[pane zone]
    FIELD --> SCHED[scheduler wake boost]

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef substrate stroke:#5b6b8c,stroke-width:2px

    class SRV,SOCK,PANE,AGENT runtime
    class DEPOSIT,FIELD,SCHED substrate

How operators use it

1. Start the server (idempotent — spawns a daemon only if none is running):

castellan multiplexer ensure
# or, Multiplexer-shaped:
castellan herd server

Default socket: ~/.config/castellan/castellan.sock (override with CASTELLAN_SOCKET or --socket).

2. Check status — the rich agent dashboard, not just “is it up”:

castellan herd status

3. Attach interactively to a pane:

castellan herd attach                    # focused pane in first workspace
castellan herd attach --pane w1:p1       # specific pane

Detach with Ctrl+Q. Use --takeover if another client already owns input.

4. Let the scheduler drive panes automatically instead of attaching by hand:

castellan run --goal "reach target" --plugin gridworld --mux
castellan swarm-demo   # scripted multi-pane demo, no goal needed

5. Wait for a pane’s agent to go idle before sending it more work (scripting/CI):

castellan herd wait --pane w1:p1 --status idle --timeout-ms 60000

6. Run over SSH when the multiplexer lives on a remote host:

castellan herd remote user@host
# or the lower-level goal runner:
castellan remote --ssh user@host --goal "reach target" --plugin gridworld

Attach modes

ModeCommandUse
Pollcastellan herd statusHeadless CI, scripts — snapshot, no session held open
Direct attachcastellan herd attachDaily-driver interactive TTY
Terminal attach (raw)castellan herd terminal attach <term_id>Attach by terminal id instead of pane label
Observe (read-only)castellan herd terminal observe --pane <id>Stream frames without taking input ownership
Named sessioncastellan herd session attach <name>Multiple independent multiplexer servers side by side

Recipes

When you want one shared pane layout across a team or CI matrix, use a named session (--session <name>) so each gets its own socket and persist directory instead of colliding on the default socket.

When an editor agent should report its own state instead of relying on screen-scraping heuristics, wire pane.report_agent from a hook script — see Agent hooks for the exact NDJSON payload and shell wrapper.

When you need to restart the multiplexer without losing your pane layout, use server.live_handoff (via the socket API) rather than killing the process — layout and visible text are preserved, but note PTY processes themselves are not preserved across handoff.

When debugging “why didn’t my mux run pick up pane activity,” confirm the run actually used --mux (in-process host with topology tracking) — without it, pane deposits never reach the scheduler’s field.

When the dashboard last_events stays empty during castellan run, ensure the mux socket is healthy (castellan multiplexer ensure). Healthy sockets receive Castellan events via events.emit; panes still use the in-process host. See Statusline.

Failure paths / troubleshooting

SymptomLikely causeFix
castellan herd status hangs or errorsNo server running, or wrong socket pathRun castellan multiplexer ensure first; check CASTELLAN_SOCKET env var
castellan herd attach immediately detachesPane doesn’t exist or was closedList panes first with castellan herd tabs; confirm the --pane id
Two terminals fight for input on the same paneBoth attached without --takeoverUse --takeover on the attach that should own input; the other becomes read-only
--mux run doesn’t show pane_tree in episode JSONRun used --no-mux, or the multiplexer socket wasn’t reachable at run startConfirm castellan herd status succeeds before the run; drop --no-mux
Server restart loses running commandsserver.live_handoff preserves layout/text only, not PTY processesExpected behavior — for long-running commands, prefer a session you don’t restart, or checkpoint work externally
Remote SSH attach failsRemote multiplexer socket not forwarded, or SSH alias misconfiguredVerify ~/.ssh/config alias resolves; try castellan remote --ssh user@host directly to isolate SSH vs. multiplexer issues

Dashboard

castellan dashboard is the ratatui operator UI over the in-tree multiplexer — agents, pressure, pane topology, and the evolution archive in one view.

What / why

Live stigmergy control tower: field heat from deposits, plan_verify / bio / cost chips, dual-bus honesty — without embedding a PTY grid (Enter still hands off to herd attach). Headless observers use the same ObservabilitySnapshot via castellan statusline refresh or ~/.config/castellan/statusline.json.

Launch

castellan multiplexer ensure
castellan dashboard

Defaults:

SettingDefaultOverride
Socket~/.config/castellan/castellan.sock--socket <PATH>
Episodes.castellan/episodes--episodes <PATH>
Archive.castellan/topology_archive.json--archive <PATH>

Anatomy

The dashboard renders four coordinated views over one DashboardState, refreshed on demand:

PaneShowsSource
Agent listPane id, agent name, status (idle/working/blocked/done/unknown)agent.list over the multiplexer socket
Pressure stripEpisode vitals and live deposit heat (live_pressure)Episodes + EventBus deposit / wake intensity
Genome tickerActive genome id, generation, lineage depth.castellan/topology_archive.json
Footer chipssocket/events/panes, plan_verify, bio, Φ, costDual-bus honesty + castellan-events
Live eventsRecent Castellan events (last_events, cap 16)Socket EventBus — castellan run / ACP via events.emit when healthy
AttachExternal PTY for selected paneEnter → herd attach (no in-TUI multi-pane grid)

Keybindings

KeyAction
j / k or / Select agent up/down
EnterAttach to selected pane (Ctrl+Q to detach)
vToggle vitals strip
rRefresh agents + layout snapshot
q / EscQuit

Recipes

Daily-driver stack:

castellan multiplexer ensure
castellan dashboard

Point at a named session (multi-project):

castellan herd session list
castellan dashboard --socket ~/.config/castellan/<session>.sock

Headless history (telemetry DB, no TUI):

castellan dashboard --history --limit 20
# same as:
castellan telemetry --limit 20

Headless live snapshot (no ratatui):

castellan statusline refresh --json | jq '{schema_version, bus, live_pressure, plan_verify, bio}'

Read a previously written file:

jq '{schema_version, connected, agents, vitals}' ~/.config/castellan/statusline.json

Headless observability

TUI and statusline refresh write the same ObservabilitySnapshot — schema in Statusline. MCP castellan_multiplexer_status adds freshness (stale, age_secs).

Honest gap: Full multiplexer chrome in the ratatui dashboard is still deferred; the current surface is agents, pressure, and pane tree.

Failure paths

SymptomCauseFix
“connection refused” on launchNo multiplexer server runningcastellan multiplexer ensure before castellan dashboard
Agent list emptyNo panes spawned yetStart an agent pane first (castellan herd attach or spawn via socket API)
Vitals strip blankNo episode has run yetcastellan run --goal "..." --plugin gridworld at least once
last_events empty while a run is activeMux socket unhealthy, or run used --no-mux / private-only pathcastellan multiplexer ensure; confirm doctor mux_default mentions socket EventBus; drop --no-mux
Genome ticker blankArchive missing or emptycastellan evolve --episodes 10 to populate .castellan/topology_archive.json
Attach hangs on Ctrl+QTerminal emulator swallowing the escape sequenceDetach with q from the agent list view instead

CI smoke

Dashboard client code is tested against a mock Unix socket:

cargo test -p castellan-dashboard --test integration

Included in ./scripts/verify.sh.

Governance overview

Castellan ships two independent governance surfaces, and neither one routes decisions through an LLM: governance is deterministic policy plus operator-configured shell hooks.

Canon (cite; don’t redesign): dual-stack diagram in repo docs/GOVERNANCE.md — Tools (PermissionPolicyplan_verify → hooks → execute) and Evolve (drift → Φ → fitness → immune).

  1. Manifest drift-checkcastellan drift-check enforces required CI checks, style files, and deprecation notices against a single source of truth (VERIFICATION_MANIFEST). See Governance policies.
  2. Runtime governance — permission presets, shell command policy, spend/risk caps, and lifecycle hooks configured in layered castellan.toml. See Agent hooks.

If you’re deciding what an agent is allowed to run, why a tool call got denied, or whether your repo passes CI’s guardrail gate, this page is the starting point — the two linked pages below go deep on each surface.

Note: "Governance" in Castellan means repo guardrails (drift-check) and runtime tool/shell policy (permissions, hooks) — not content moderation or multi-agent arbitration.

Anatomy: the two surfaces

SurfaceQuestion it answersEnforced byConfig
Manifest drift-checkDoes the repo still satisfy required CI/style/doc guardrails?castellan drift-check, scripts/drift-guard.shcrates/castellan-governance/src/manifest.rs (code, not TOML)
Permission policyIs this MCP tool call / shell command allowed right now?ToolGovernancePipeline, ShellSandbox[permissions], [shell] in castellan.toml
Spend / risk capsHas this session exceeded budget or risk thresholds?ToolGovernancePipeline[governance] in castellan.toml
Lifecycle hooksWhat deterministic shell command fires on session/tool events?castellan run, MCP tool calls[[hooks.hooks]] in castellan.toml
flowchart TB
    CFG[castellan.toml layers] --> PERM[permission preset]
    CFG --> SHELL[shell allow/deny lists]
    CFG --> CAP[spend + risk caps]
    CFG --> HOOKS[lifecycle hooks]

    PERM --> PIPE[ToolGovernancePipeline]
    SHELL --> PIPE
    CAP --> PIPE
    HOOKS --> PIPE

    PIPE --> RUN[castellan run / mcp]
    RUN --> AUDIT[audit JSONL]

    MANIFEST[VERIFICATION_MANIFEST] --> DRIFT[castellan drift-check]
    DRIFT --> CI[CI gate]

    classDef gate stroke:#1a7f37,stroke-width:2px
    classDef runtime stroke:#0969da,stroke-width:2px

    class PERM,SHELL,CAP,HOOKS,MANIFEST,DRIFT gate
    class PIPE,RUN,AUDIT,CI runtime

Permission presets

[permissions] preset expands into a base policy before any [permissions.tools] overrides apply:

PresetDefault modeUse when
cautious (aka always-ask)Prompts on nearly everything, including meta-tools like castellan_discover_toolsFirst install, unfamiliar plugin, shared/untrusted repo
balanced (aka write)Allows read-ish tools, asks before writes/mutationsDaily-driver operator loop once you trust the plugin set
permissive (aka yolo)Allows by defaultCI, sandboxed containers, fully scripted recipes

Prompt mode UX

PermissionMode::Prompt is resolved through a host permission prompter seam — not a silent deny.

SurfaceBehavior
castellan run on a TTYInteractive [y/N] on stderr for each prompted tool
castellan run non-TTYFail closed (deny) with a clear reason + PermissionDenied event
Scripted / CISet CASTELLAN_PERMISSION_PROMPT=allow to auto-allow (audited via reason/events), or =deny to force deny
ACP permissions/requestReturns needs_prompt: true for the editor client to present UI
ACP session/prompt engine pathSame finalize seam; headless default deny unless CASTELLAN_PERMISSION_PROMPT=allow

Prompt decisions always emit observability (PermissionDenied / ToolDecision) — never a silent drop.

How operators use it

1. Check the current runtime governance posture:

castellan doctor --json | jq '.stages[] | select(.name=="governance_manifest")'

2. Run the manifest guardrail check (fast, no network, what CI runs):

castellan drift-check

3. Set a permission preset for daily-driver operation:

castellan config set permissions.preset balanced

4. Override a single tool’s policy without changing the whole preset:

[permissions.tools]
castellan_propose_mutation = "deny"
castellan_deposit_signal = "allow"

5. Cap spend and risk for a session in castellan.toml:

[governance]
session_spend_cap_usd = 12.5
max_risk_score = 0.85
denied_paths = ["/secret", "~/.ssh"]

6. Record every governed decision to an audit trail for a run:

castellan run --goal "reach target" --plugin shell --audit .castellan/audit.jsonl

Recipes

When onboarding a new repo or plugin, start with preset = "cautious" and loosen to balanced once you’ve watched a few runs and trust the shell/tool surface.

When running in CI or a disposable container, use preset = "permissive" but keep [shell] denied_commands non-empty — permissive changes the default, not the explicit denylist.

When you need a paper trail for a specific run (compliance, debugging a bad tool call), always pass --audit <path> — governance decisions aren’t persisted anywhere else by default.

When a PR touches CI, manifest.rs, or lint/toolchain config, run castellan drift-check locally before pushing — the same check gates CI and failures there block merge.

Failure paths / troubleshooting

SymptomLikely causeFix
castellan drift-check fails locally but code looks fineA guardrail file (CI workflow, manifest.rs, toolchain pins) drifted from what the manifest expectsRead the check_id/message in the failure; fix the guardrail file, don’t suppress the check
Tool call unexpectedly deniedpreset is cautious, or a [permissions.tools] override sets denyCheck castellan doctor --json for the active preset; add an explicit allow override if appropriate
Prompted tool denied with “no prompter” / “auto-denied”Non-TTY run without CASTELLAN_PERMISSION_PROMPT=allow, or headless ACP engine pathRun on a TTY, set CASTELLAN_PERMISSION_PROMPT=allow for CI, or set the tool to allow in [permissions.tools]
Shell plugin command blockedMatches [shell] denied_commands or denied_patternsConfirm the command is actually safe, then adjust the denylist — don’t bypass via a wrapper script
Session aborts with a spend/risk errorsession_spend_cap_usd or max_risk_score exceededRaise the cap deliberately in castellan.toml, or investigate why the run is spending/risking more than expected
Hook command fails the whole operationHook script exited non-zeroHook commands must exit 0; fix the script or remove the hook if it’s non-critical

Governance policies

Policies that keep Castellan’s codebase and guardrails from drifting apart.

Verification pipeline

.github/workflows/ci.yml
        │
        ├── scripts/drift-guard.sh  →  castellan drift-check (manifest)
        └── scripts/verify.sh       →  fmt, clippy, hack, test, build, smoke

CI must call ./scripts/drift-guard.sh then ./scripts/verify.sh only. Do not add duplicate lint or test steps to the workflow — that creates anti-drift debt.

Single source of truth

ConcernCanonical location
Required checks listcrates/castellan-governance/src/manifest.rs
Check enforcementcastellan drift-check (also invoked by drift-guard.sh)
Full verificationscripts/verify.sh
Code styleEngineering guide
License / advisory policydeny.toml

When adding a new required check:

  1. Add a ManifestCheck entry to manifest.rs.
  2. Implement validation in run_drift_check.
  3. If the check belongs in the full suite, add it to verify.sh.
  4. Note the change in your PR description (required).

PR requirements

PRs that touch any of the following must include a ## Guardrails section in the description explaining what changed and why:

  • scripts/verify.sh
  • scripts/drift-guard.sh
  • .github/workflows/**
  • crates/castellan-governance/src/manifest.rs
  • rust-toolchain.toml, clippy.toml, rustfmt.toml, deny.toml

PRs that change Rust style conventions must update the engineering guide in the same PR.

Drift-check command

cargo run -p castellan-cli -- drift-check
# or, after install:
castellan drift-check

Exits non-zero when any VERIFICATION_MANIFEST expectation fails. Use locally before pushing when editing guardrail files.

Python deprecation

The python/ tree is deprecated and packages were removed. python/README.md must retain a deprecation notice — drift-check enforces this when the directory exists.

Review expectations

  • Guardrail changes: one maintainer approval minimum.
  • Manifest changes: confirm castellan drift-check and ./scripts/verify.sh both pass in CI.
  • No allow(dead_code) in crate sources without removing the check from manifest (not permitted).

Agent hooks

Castellan supports two hook surfaces: governance lifecycle hooks (configured in castellan.toml) and multiplexer agent lifecycle reporting (socket API for editor agents).

Governance lifecycle hooks (castellan.toml)

Configure deterministic shell hooks in layered config (castellan.toml, ~/.castellan/config.toml, .castellan/local.toml):

[[hooks.hooks]]
event = "SessionStart"
command = "./scripts/hooks/session-start.sh"

[[hooks.hooks]]
event = "PreToolUse"
command = "./scripts/hooks/pre-tool.sh"

[[hooks.hooks]]
event = "PostToolUse"
command = "./scripts/hooks/post-tool.sh"

[[hooks.hooks]]
event = "SessionEnd"
command = "./scripts/hooks/session-end.sh"

Accepted event / kind values: SessionStart, SessionEnd, PreToolUse, PostToolUse (snake_case aliases also work).

Each command receives a single JSON payload line on stdin:

EventPayload fields
SessionStartgoal_id, plugin
SessionEndgoal_id, status
PreToolUsetool, argv, agent_id
PostToolUsetool, argv, success

Invocation paths

PathWhen hooks run
castellan runSessionStart/SessionEnd on episode boundaries; PreToolUse/PostToolUse on MCP tool calls and shell plugin execution via ToolGovernancePipeline
castellan run --rlmSame session hooks; mux delegate steps use nested sub_harness panes
Shell pluginPreToolUse/PostToolUse with tool: "shell" and argv in payload

Hook commands must exit 0; non-zero exits fail the surrounding operation.

Prove-before-execute ([governance.plan_verify])

Optional Guardians-shaped gate (Meijer / metareflection pattern — not Universalis-the-language). When enabled, high-risk tools (shell / exec by default) require a verified workflow_plan certificate in the tool args envelope before side effects. Missing or failing verification ⇒ deny (fail closed). Composes under PermissionPolicy: Deny still short-circuits; plan verify does not replace Allow / Deny / Prompt.

[governance.plan_verify]
enabled = true
# high_risk_tools = ["shell", "exec"]
# allowlisted_tools = ["shell", "exec", ...]

Supply the plan on shell/MCP args as workflow_plan (or tool_workflow): ordered steps with symbolic refs (no live payloads). Research: docs/superpowers/research/2026-07-13-universalis-agent-proofs.md. Demo: docs/demo/plan-prove-before-execute.sh.

This is tool-plan safety on the PreToolUse path — distinct from evolve drift / Φ / immune gates on topology genomes. When plan verify runs on the MCP tool hot path, outcomes emit PlanVerifyPassed / PlanVerifyDenied on the live EventSink (CLI --json, mux bridge, ACP) so observers see prove-before-execute without reading audit JSONL alone.

Multiplexer agent lifecycle (socket API)

Agents (Claude Code, Codex, Cursor) can report lifecycle state to the multiplexer without relying on screen heuristics.

Socket API

{"id":"1","method":"pane.report_agent","params":{
  "pane_id": "w1:p1",
  "agent": "claude",
  "state": "working",
  "source": "hook"
}}

States: idle, working, blocked, done, unknown.

When source is hook, screen heuristics are ignored until the pane is reset.

Shell hook example

Install into your agent wrapper (~/.config/castellan/hooks/herdr-agent-state.sh — historical script name):

#!/usr/bin/env bash
# Usage: herdr-agent-state.sh <pane_id> <agent> <state>
PANE_ID="${1:?pane_id}"
AGENT="${2:?agent}"
STATE="${3:?state}"
SOCKET="${CASTELLAN_SOCKET:-$HOME/.config/castellan/castellan.sock}"
printf '%s\n' "{\"id\":\"hook\",\"method\":\"pane.report_agent\",\"params\":{\"pane_id\":\"$PANE_ID\",\"agent\":\"$AGENT\",\"state\":\"$STATE\",\"source\":\"hook\"}}" \
  | nc -U "$SOCKET"

Claude / Codex / Cursor integration

AgentSuggested hook point
Claude CodeWrap claude in a shell function; call hook on tool-approval and turn-complete
CodexExport CODEX_HOOK_CMD pointing to the script above
CursorUse cursor-agent lifecycle env callbacks if available; else poll is fallback

CLI wait helper

castellan herd wait --pane w1:p1 --status idle --timeout-ms 60000

Equivalent to events.wait with pane_agent_status_changed.

Environment

VariablePurpose
CASTELLAN_SOCKETOverride default ~/.config/castellan/castellan.sock
CASTELLAN_ENV=1Set in spawned pane shells (detect castellan-managed sessions)

Plugin architecture

A plugin is a goal environment adapter: it encodes goals into runnable tasks, verifies observations against goals, and scores outcomes for evolution. Plugins are what make Castellan’s coordination and evolution loops generic — the scheduler, substrate, and evolve pipeline never know anything about gridworlds or shell commands specifically; they only know the CastellanPlugin trait.

If you’re integrating a new environment, debugging why fitness looks wrong for your plugin, or deciding between install and link while iterating, this page is the reference.

The trait

#![allow(unused)]
fn main() {
#[async_trait]
pub trait CastellanPlugin: Send + Sync {
    fn name(&self) -> &'static str;
    fn encode_task(&self, goal: &Goal) -> TaskDescriptor;
    async fn verify_goal(&self, goal: &Goal, observation: &serde_json::Value) -> AgentReport;
    fn fitness(&self, goal: &Goal, report: &AgentReport) -> FitnessVector;
    fn tools(&self) -> Vec<ToolSpec> { vec![] }
}
}
MethodCalled whenProduces
name()Registry lookup, episode environment_fingerprintStable plugin identifier
encode_task(goal)Run startInitial TaskDescriptor — what the scheduler dispatches first
verify_goal(goal, observation)Every tick after an observation landsAgentReport with a NextAction (complete / retry / delegate / fail)
fitness(goal, report)Episode endFitnessVector — plugin-grounded score consumed by castellan evolve
tools()Registry initOptional ToolSpec list the plugin exposes to MCP-connected agents

Supporting traits, for narrower integrations:

  • GoalVerifier — verify-only surface, used by RLM loops (castellan run --rlm) that don’t need the full plugin lifecycle.

Coordination hooks (optional, provided via context at runtime — not part of the trait itself):

  • PluginContext — blackboard read/write, budget snapshot.
  • CoordinationContext — zone list, perception radius, deposit_signal / read_signal.

Anatomy: lifecycle

flowchart LR
    REG[register] --> ENC[encode_task]
    ENC --> SCHED[scheduler run]
    SCHED --> VER[verify_goal]
    VER --> EP[episode snapshot]
    EP --> EVOLVE[evolve fitness]

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef evolve stroke:#d97706,stroke-width:2px

    class REG,ENC,SCHED,VER runtime
    class EP,EVOLVE evolve
  1. RegisterPluginRegistry::builtin() loads built-ins plus any manifest plugin.toml on disk.
  2. Encodeencode_task(goal) produces the initial TaskDescriptor payload the scheduler dispatches.
  3. Run — the scheduler enqueues the task in the plugin’s primary zone; the host executes it; observations flow back into the blackboard/field.
  4. Verifyverify_goal returns an AgentReport carrying a NextAction (complete, retry, delegate, or fail).
  5. Episode — the runtime writes .castellan/episodes/<goal-id>.json, including the plugin’s fitness() output.
  6. Evolvecastellan evolve reads the episode corpus; the plugin name seeds the archive’s environment_fingerprint lookup for future --seed-archive runs.

Current built-in plugins

PluginKindLLM requiredDescription
gridworldSimulatorNoDeterministic 5×5 grid; agents navigate to a target; boids-style coordination deposits
shellExecutorNoBounded subprocess (argv, cwd, timeout); exit code + output verification

Plugin manifest (plugin.toml)

name = "gridworld"
description = "Deterministic 5x5 grid navigation simulator"
version = "0.1.0"

[zones]
primary = "grid"
default = ["grid", "goal"]

[blackboard]
slots = ["state", "last_observation"]

Manifests live at crates/castellan-plugins/<name>/plugin.toml. The registry scans that tree at init — you never hand-edit a central registry.rs to add a plugin.

How operators use it

1. List what’s registered:

castellan plugin list

2. Inspect a plugin’s manifest, zones, and blackboard schema:

castellan plugin info gridworld

3. Scaffold a new plugin from the template:

castellan plugin new myplugin
castellan plugin list
castellan run --goal "your goal" --plugin myplugin

4. Iterate on a plugin without a full reinstall — symlink instead of copy:

castellan plugin link --from ~/src/my-plugin
cargo build --release -p castellan-cli   # rebuild to register
# edit ~/src/my-plugin/src/lib.rs, rebuild — no re-link needed

5. Install a plugin from git (for sharing/distribution):

castellan plugin install --git https://github.com/example/castellan-gridworld-extra.git
cargo build --release -p castellan-cli   # rebuild to register

6. Uninstall:

castellan plugin uninstall myplugin

install deletes the copied source tree; link only removes the symlink and leaves your source directory intact.

installlink
Source locationCopied into crates/castellan-plugins/<name>Symlinked — edits in the original directory are live
IterationRe-install after every changeRebuild only (cargo build)
UninstallDeletes the copyRemoves the symlink; source directory intact
Best forDistribution, committing to the repoActive plugin development

castellan plugin list reports "source": "builtin" | "install" | "link" per plugin so you can tell at a glance how each one got there.

Extension points

ExtensionLocation
Pluginscastellan-plugin-api, castellan-plugins
RunHost backendscastellan-runtime::RunHost, castellan-rah, castellan-herdr
Evolve hookscastellan-evolveGepaHook, EpisodeLogProposer
Governancecastellan-governanceVERIFICATION_MANIFEST
Multiplexercastellan-multiplexer — pane deposits → observations

Recipes

When building a new environment adapter, start from castellan plugin new myplugin rather than hand-rolling the manifest — the template wires up [zones]/[blackboard] correctly and registers automatically on rebuild.

When your plugin’s fitness always comes back flat, check fitness(goal, report) against the actual AgentReport.next_action values you’re returning from verify_goal — a fitness function that ignores NextAction variants will look identical across very different outcomes.

When two plugins need to share coordination state, give them overlapping default zones deliberately in each plugin.toml, but keep primary zones distinct so scheduling doesn’t collide.

When distributing a plugin to teammates, publish it as a git repo and have them castellan plugin install --git <url> rather than sharing a local path — link is a dev-loop convenience, not a distribution mechanism.

Failure paths / troubleshooting

SymptomLikely causeFix
unknown plugin: <name> from castellan runPlugin not registered (missing plugin.toml, or not rebuilt after install/link)castellan plugin list to confirm registration; rebuild after install/link
castellan plugin new output doesn’t show up in plugin listRegistry discovery happens at build time via build.rs, not at runtimeRun cargo build --release -p castellan-cli after scaffolding
Linked plugin edits don’t take effectForgot to rebuild after editing sourcecargo build — link makes the source live, not the compiled binary
Plugin’s episodes never seed an archive entryenvironment_fingerprint mismatch between evolve and run (different plugin name or goal shape)Confirm name() is stable and goal text matches what evolve saw
verify_goal never returns CompletePlugin logic bug, or observation shape doesn’t match what verify_goal expectsAdd tracing/log output inside verify_goal; replay the episode with castellan replay to inspect observations tick-by-tick

CLI overview

Castellan is a single binary (castellan) with subcommands for goal runs, evolution, multiplexer control, MCP, and diagnostics.

Precedence

Configuration resolves in this order (highest wins):

  1. CLI flags — e.g. --goal, --plugin, --json
  2. Environment variables — e.g. CASTELLAN_MUX_AUTO, CASTELLAN_SESSION_SPEND_CAP_USD
  3. Layered config files — see castellan.toml

Output modes

ModeFlag / entrypointConsumerReference
Human textdefaultInteractive terminalrun
NDJSON eventscastellan run --jsonCI, automation, log pipelinesrun
MCP stdiocastellan mcpCursor, Claude Desktop, other MCP clientsmcp, MCP tools
ACP stdiocastellan acpEditor-class agents (Zed, experimental)ACP integration
Socket NDJSONcastellan multiplexer serverDashboard, remote attach, mux automationSocket API

One runnable recipe per mode:

# text — human terminal
castellan run --goal "reach target" --plugin gridworld

# json — typed NDJSON events for CI (episode JSON also lands in .castellan/episodes/)
castellan run --goal "reach target" --plugin gridworld --json | grep episode_end

# mcp — stdio JSON-RPC (tools/list handshake)
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n' | castellan mcp

# socket — multiplexer NDJSON API
castellan multiplexer server &
castellan multiplexer status

# acp — agent client protocol over stdio
castellan acp doctor

Episode JSON is always written to .castellan/episodes/<goal-id>.json on successful runs (separate from --json stdout).

Top-level commands

CommandPage
runrun
evolveevolve
remoteremote
herdherd — mux / pane UX (historical name; see lexicon)
pluginplugin
mcpmcp
telemetrytelemetry
doctordoctor
replayreplay
swarm-demo, rah-demodemos — multi-pane / RAH demos

Other commands: multiplexer, acp, skills, episodes, recipes, drift-check, memory, integration, vitals, genome, dashboard, completions, ipc (deprecated). Run castellan <cmd> --help for flags. Recipes are defined in castellan.toml [[recipes]].

Shell completions

Completions are generated from live clap metadata so they stay in sync with the CLI:

castellan completions bash   # bash completion script
castellan completions zsh    # zsh completion script
castellan completions fish   # fish completion script

Install examples:

# bash (user-local)
mkdir -p ~/.local/share/bash-completion/completions
castellan completions bash > ~/.local/share/bash-completion/completions/castellan

# zsh
castellan completions zsh > "${fpath[1]}/_castellan"

# fish
mkdir -p ~/.config/fish/completions
castellan completions fish > ~/.config/fish/completions/castellan.fish

Re-run after upgrading castellan when subcommands or flags change.

Help snapshots

CLI help is snapshot-tested in CI. After intentional CLI changes:

UPDATE_SNAPSHOTS=1 cargo test -p castellan-cli cli_help_snapshot

See Deployment.

Recipes

castellan run --goal "reach target" --plugin gridworld
castellan run --goal "reach target" --plugin gridworld --json | grep episode_end
castellan multiplexer server &
castellan dashboard &
castellan run --goal "reach target" --plugin gridworld --mux
castellan evolve --episodes 16 --workspace .castellan/episodes
castellan run --goal "reach target" --plugin gridworld --seed-archive

castellan run

Scheduler-led goal loop with a plugin verifier. Writes episode JSON to .castellan/episodes/<goal-id>.json.

Usage

castellan run --goal <GOAL> [--plugin gridworld] [--resume [<GOAL_ID>]]
          [--rlm] [--max-steps 8]
          [--audit <PATH>] [--json] [--mcp] [--model <URL_OR_NAME>]
          [--genome <ID>] [--seed-archive] [--archive .castellan/topology_archive.json]
          [--write-back] [--no-write-back] [--mux] [--no-mux]

When resuming, --goal is optional (defaults to checkpoint description). Plugin comes from the checkpoint.

Key flags

FlagDefaultNotes
--goalrequired unless --resumeNatural-language goal text
--plugingridworldVerifier plugin (castellan plugin list)
--resumeoffContinue from .castellan/checkpoints/ (optional goal UUID; default latest)
--rlmoffIn-tree RLM verify/act loop
--max-steps8Max RLM steps when --rlm
--jsonoffNDJSON castellan-events on stdout
--mcpoffProcess mcp_tools from observation payload
--muxoff (explicit)Force scheduler-led run via in-process multiplexer panes
--no-muxoffRollback for mux-by-default; also CASTELLAN_MUX_AUTO=0 or CASTELLAN_MUX_DISABLE=1
--seed-archiveoffSeed topology from evolve archive before run
--write-backauto with --seed-archiveMerge fitness into archive after success

--mux runs via MuxHarnessHost; episode JSON includes pane_tree. Mux is the default when the multiplexer socket is healthy — no CASTELLAN_MUX_AUTO=1 required. Rollback with --no-mux, CASTELLAN_MUX_AUTO=0, or CASTELLAN_MUX_DISABLE=1.

--json events

EventSummary
episode_startgoal_id, plugin, goal
scheduler_ticktick, pending
task_dispatchedtick, task_id, zone
verify_resulttick, zone, goal_met
cost_rollupper-tick usd, tokens, session total_usd, total_tokens
depositzone signal deposit
archive_write_backoptional genome merge
episode_endgoal_id, status, fitness, episode_path

See statusline for dashboard consumption.

Recipes

Gridworld smoke (verify.sh default):

castellan run --goal "reach target" --plugin gridworld

RLM verify/act:

castellan run --goal "reach target" --plugin gridworld --rlm --max-steps 4

Resume mid-goal:

castellan run --goal "reach target" --plugin gridworld
castellan run --resume

Mux topology in episode:

castellan run --goal "reach target" --plugin gridworld --mux

Evolve round-trip:

castellan evolve --episodes 5 --workspace .castellan/episodes
castellan run --goal "reach target" --plugin gridworld --seed-archive --write-back

castellan evolve

Evolve harness topology from episode logs. Optional reflective Python hook; drift gate rejects incompatible mutations.

Usage

castellan evolve [--episodes 10] [--workspace .castellan/episodes] [--hook rust|seed-only] [--summarize]
castellan evolve --plan
castellan evolve --dry-run
castellan evolve --apply-proposal <UUID>

Key flags

FlagDefaultNotes
--episodes10Generations to run
--workspace.castellan/episodesEpisode JSON corpus directory
--hookrustProposer hook (seed-only for fixture tests)
--summarizeoffWrite .castellan/episode_summary.json without evolving
--planoffRead-only analysis: emit an evolution plan JSON, zero writes
--dry-runoffPersist proposals to .castellan/proposals/, no archive writes
--apply-proposalResolve one persisted proposal (drift gate re-checked)

--summarize compacts the corpus for the proposer (DRY with substrate heatmap). Drift-incompatible candidates are rejected and logged.

Plan vs dry-run vs apply

ModeReads corpusWrites proposalsWrites archive
--planyesnono
--dry-runyesyesno
--apply-proposalyesresolves oneon accept

Use --plan for a pre-mutation analysis turn (incumbent, proposed coordination, pressure summary, drift preview, fitness estimate), then --dry-run + --apply-proposal (or the MCP castellan_propose_mutation / castellan_resolve_mutation pair) for the gated two-phase apply.

Recipes

Standard evolve from local episodes:

castellan run --goal "reach target" --plugin gridworld
castellan evolve --episodes 10 --workspace .castellan/episodes

Summarize only (no mutation):

castellan evolve --summarize --workspace .castellan/episodes

Seed archive then run:

castellan evolve --episodes 10
castellan run --goal "reach target" --plugin gridworld --seed-archive

castellan herd

Multiplexer UX over the in-tree PTY server — attach, status, and pane operations without raw socket JSON. The subcommand name is historical; in prose this is mux / pane control, not an animal metaphor. Verified against HerdCommands in crates/castellan-cli/src/main.rs.

Usage

castellan herd <SUBCOMMAND>

Subcommands

SubcommandFlagsPurpose
server--socket <PATH>, --session <NAME>Start or attach to the multiplexer server
status--socket <PATH>, --notifyRich agent dashboard JSON (--notify sends a macOS notification when agents are blocked)
workspaces--socket <PATH>List workspaces on the multiplexer
tabs--socket <PATH>, --workspace <ID>List tabs in a workspace (default: focused)
attach--socket <PATH>, --pane <ID>, --session <NAME>Attach terminal UI to a pane (Ctrl+Q to detach); default is focused pane
wait--socket <PATH>, --pane <ID>, --status <STATUS> (default idle), --timeout-ms <MS> (default 30000)Block until an agent reaches a status (events.wait wrapper)
agent explain--socket <PATH>, --pane <ID>, --verboseExplain why a pane has its current status, with matched rules
agent attach--socket <PATH>, --takeover, <TARGET>Attach directly to an agent by name, label, or pane id
session list--jsonList named session servers
session attach--socket <PATH>, <NAME>Attach to a named session server
session stop--json, <NAME>Stop a named session server (persists snapshot)
session delete--json, <NAME>Delete a named session and its data
terminal attach--socket <PATH>, --takeover, <TERM_ID>Direct attach to a terminal by id
terminal observe--socket <PATH>, --pane <ID>, --cols, --rowsRead-only NDJSON terminal.frame stream
terminal control--socket <PATH>, --pane <ID>, --takeover, --cols, --rowsWritable NDJSON terminal (frames out, commands on stdin)
remote<HOST>, --session, --handoff, --remote-keybindings local|server, --socketSSH thin-client remote attach

Run castellan herd <subcommand> --help for the current flag list — this table tracks main.rs at time of writing.

Recipes

Check multiplexer health:

castellan herd status

Attach to a running agent by name:

castellan herd agent attach claude

Explain why an agent looks stuck:

castellan herd agent explain --pane w1:p1 --verbose

Wait for an agent to go idle (scripting):

castellan herd wait --pane w1:p1 --status idle --timeout-ms 60000

Named sessions (multi-project):

castellan herd session list --json
castellan herd session attach my-project
castellan herd session stop my-project

Read-only pane observation (headless monitor):

castellan herd terminal observe --pane w1:p1 --cols 100 --rows 30

Remote mux panes over SSH:

castellan herd remote user@host --session my-project

Daily-driver stack:

castellan multiplexer ensure
castellan herd status
castellan dashboard

Failure paths

SymptomCauseFix
herd status returns "running": falseNo server at the socket pathcastellan multiplexer ensure, then retry
herd wait times outAgent never reached target status within --timeout-msIncrease timeout, or herd agent explain --pane <id> to see why it’s stuck
herd attach shows nothingWrong --pane/--session, or pane exitedherd tabs / herd workspaces to enumerate valid ids first
herd remote fails to connectSSH auth or ~/.ssh/config alias missingTest ssh <host> directly before wrapping in herd remote
Ctrl+Q doesn’t detachTerminal emulator intercepts the escape sequenceUse q from castellan dashboard’s agent list instead

castellan remote

Attach to the in-tree multiplexer and run a governed goal on remote panes.

Usage

castellan remote --goal <GOAL> [--plugin gridworld] [--socket <PATH>]
             [--ssh user@host] [--stub] [--audit <PATH>]

Key flags

FlagNotes
--goalRequired goal text
--pluginVerifier plugin (default gridworld)
--socketMultiplexer NDJSON socket path
--sshRemote host for SSH attach
--stubDeterministic stub run (CI smoke)
--auditOptional governance audit JSONL

Governance pipeline (with_governance) applies on the remote hot path.

Recipes

Local stub smoke (verify.sh):

castellan remote --stub --goal "reach target" --plugin gridworld

Remote with audit trail:

castellan remote --goal "refactor auth module" --plugin shell --audit .castellan/audit.jsonl

castellan plugin

List, inspect, scaffold, install, link, and uninstall dynamic plugins.

Usage

castellan plugin list
castellan plugin info <NAME>
castellan plugin new <NAME>
castellan plugin install --from <DIR>
castellan plugin install --git <URL> [--name <NAME>]
castellan plugin link --from <DIR> [--name <NAME>]
castellan plugin uninstall <NAME>

Installed plugins register via plugins.toml and build.rs — no hand-editing registry.rs.

installlink
Sourcescopied into crates/castellan-plugins/<name>symlinked — edits are live
Iteratere-install after every changerebuild only (cargo build)
Uninstalldeletes the copyremoves the symlink, sources intact

castellan plugin list reports "source": "builtin" | "install" | "link" per plugin.

Recipes

List registered plugins:

castellan plugin list

Install from local directory:

castellan plugin install --from ./my-plugin
cargo build --release -p castellan-cli   # rebuild to register

Iterate on a plugin without reinstall:

castellan plugin link --from ~/src/my-plugin
cargo build --release -p castellan-cli   # rebuild to register
# edit ~/src/my-plugin/src/lib.rs, rebuild — no re-link needed
castellan plugin uninstall my-plugin     # removes only the symlink

Install from git:

castellan plugin install --git https://github.com/example/castellan-gridworld-extra.git

Uninstall:

castellan plugin uninstall sample-plugin

castellan mcp

MCP stdio JSON-RPC server exposing stigmergy, memory, and harness tools from the shared registry (12 operational tools + 3 meta tools — see MCP tools reference).

Usage

castellan mcp [--attach <goal_id|auto>]

Methods: initialize, tools/list, tools/call. Same registry as castellan run --mcp live episodes — no drift between editor and headless behavior.

Attach mode (live field instrument)

--attach binds the stdio server to a running castellan run --mcp goal via the .castellan/live/ session manifest:

  • --attach auto binds to the current live session; --attach <goal_id> requires that exact goal to be live.
  • Fails loudly (attach failed: no live session) when nothing is running — no silent standalone fallback.
  • Deposits and snapshots route into the live field with source: "mcp_attach" provenance, which lands in the episode’s instrument_summary.
  • Mutation tools (castellan_propose_mutation, castellan_resolve_mutation) are denied in attach mode; attached clients are instruments, not operators.
  • If the session ends mid-attach, live tools return an attach_lost error.
castellan run --goal "demo" --mcp &   # session A: live engine
castellan mcp --attach auto           # session B: attached instrument

castellan doctor reports the live session state in its live_session stage.

Error responses

ErrorMeaningCause
-32601Method/tool not foundTypo, or tool not yet added to the registry
-32602Invalid paramsMissing/wrong-typed required field — call castellan_describe_tool for the schema

castellan_search_tools queries are capped at 256 characters.

Recipes

List tools (stdio):

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | castellan mcp

Discovery flow:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"castellan_search_tools","arguments":{"query":"deposit"}}}' \
  | castellan mcp

Describe before invoking (avoid -32602):

echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"castellan_describe_tool","arguments":{"name":"castellan_deposit_signal"}}}' | castellan mcp

Wire Cursor: see Cursor setup.

castellan telemetry

Query dashboard telemetry database — sessions, agent transitions, vitals snapshots, and per-tick cost rollups — without a TTY.

Usage

castellan telemetry [--limit 20] [--db <PATH>]
castellan dashboard --history [--limit 20]

Default DB: ~/.config/castellan/telemetry.db.

Key flags

FlagDefaultNotes
--limit20Max rows per query
--dbuser config pathOverride telemetry database

Summary JSON includes cost_rollup_count, session_cost_usd, session_tokens, and recent_costs (from governance cost_rollup wire events recorded while the dashboard is open).

Recipes

Headless session list:

castellan telemetry --limit 5

Dashboard history JSON:

castellan dashboard --history --limit 10

castellan doctor

Staged runtime diagnostics: multiplexer, governance, config layers, LLM provider, plugins, evolve archive, latest episode vitals. Verified against DoctorReport::run in crates/castellan-cli/src/doctor_cmd.rs12 stages, always run in this order.

Usage

castellan doctor [--workspace .] [--json]

--json does not change exit behavior — doctor always exits non-zero if any stage’s pass is false. Not every stage can fail: config/preset stages (permissions_preset, plan_verify, model_tiers, mux_default) always report informational detail with pass: true. llm_provider fails when provider = "openrouter" and no API key env is set. memory_sync fails when sync is configured without a token env. Only multiplexer_socket, governance_manifest, live_session, plugin_registry, evolve_archive, and episode_vitals can fail for other reasons.

Stages

Order matches DoctorReport::run in doctor_cmd.rs:

StageCan failChecks
multiplexer_socketyesDefault socket exists / accepts connections
mux_defaultno (informational)Whether mux-by-default is on given socket health + env vars
governance_manifestyescastellan drift-check violations against the verification manifest
permissions_presetno (informational)Active [permissions] preset and tool-override count
plan_verifyno (informational)Guardians-shaped [governance.plan_verify] enabled state
model_tiersno (informational)[models] tier config (default/verify/act/plan — scheduler never uses LLM)
llm_provideryes (openrouter without key)Provider, base_url, key_present (never the value), optional headers, TCP reachability
memory_syncyes (configured, no token)[memory.sync] Turso remote-sync health
live_sessionyesActive live-field session snapshot / idle + instrument corpus note
plugin_registryyesRegistry has at least one plugin, matches plugins.toml
evolve_archiveyes.castellan/topology_archive.json under --workspace is readable
episode_vitalsyesLatest episode under .castellan/episodes has circulation_vitals or wake_stats

Recipes

Local workspace check:

castellan doctor --workspace .

CI-friendly JSON:

castellan doctor --json | jq '.stages[] | select(.pass == false)'

Human table (default):

STAGE                  STATUS DETAIL
------------------------------------------------------------------------
multiplexer_socket      PASS  socket present at /Users/you/.config/castellan/castellan.sock
mux_default             PASS  mux default on (socket_healthy=true); rollback: --no-mux, ...
governance_manifest     PASS  verification manifest OK
...

Failure paths

Stage failingDetail message patternFix
multiplexer_socketsocket missing at ... (run castellan multiplexer ensure)castellan multiplexer ensure
governance_manifestN drift violation(s)castellan drift-check for the full list; fix castellan.toml
llm_providerprovider=openrouter ... key_present=falseexport OPENROUTER_API_KEY=... (never commit the key)
memory_syncconfigured without token envSet the documented Turso token env
plugin_registryempty plugin listCheck plugins.toml; castellan plugin list
evolve_archivearchive missing or invalidRun castellan evolve at least once, or pass --workspace to the right root
episode_vitalsno episode JSON in ...castellan run --goal "..." --plugin gridworld at least once

castellan replay

Replay a saved episode JSON or audit JSONL file. --continue-run resumes from checkpoint when supported.

Usage

castellan replay <PATH> [--continue-run]

Recipes

Inspect last episode:

castellan replay .castellan/episodes/latest.json

Replay audit trail:

castellan replay .castellan/audit.jsonl

Demo commands

Scheduler and RAH demos for verify.sh and local exploration.

castellan swarm-demo

Multi-pane scheduler-led coordination demo (swarm-demo is the CLI name).

castellan swarm-demo

Expects output with pane_count, mux_host, and sub_harness_depth (verify.sh smoke).

castellan rah-demo

Depth-3 RAH pane spawn demo — multiplexer + topology binding.

castellan rah-demo

Expects "pane_tree_depth": 3 and "episode_pane_tree": true.

Recipe: demo → evolve flywheel

castellan run --goal "reach target" --plugin gridworld --mux > .castellan/episodes/mux-run.json
castellan evolve --episodes 3 --workspace .castellan/episodes

CLI reference

Single binary: castellan. Paths and config use .castellan/ and castellan.toml. MCP tools are named castellan_*.

Start here: CLI overview

CommandPage
runrun
evolveevolve
herdherd — mux / pane UX (historical name)
remoteremote
pluginplugin
mcpmcp
doctordoctor
replayreplay
telemetrytelemetry
Demosdemos

Also: multiplexer, dashboard, statusline, drift-check, acp, skills, episodes, recipes, config, memory, vitals, genome, completions. Run castellan <cmd> --help for flags.

Help snapshots: UPDATE_SNAPSHOTS=1 cargo test -p castellan-cli cli_help_snapshot

castellan.toml reference

Layered configuration for governance, shell policy, hooks, and MCP permissions. Schema source: crates/castellan-governance/src/config.rs.

File locations (merge order)

LayerPathPrecedence
User~/.castellan/config.tomllowest
Projectcastellan.toml (walk up from cwd)middle
Local.castellan/local.toml (same repo as project castellan.toml)highest

Later layers override earlier ones. CLI flags and environment variables override all files.

Example

[governance]
session_spend_cap_usd = 12.5
max_risk_score = 0.85
denied_paths = ["/secret", "~/.ssh"]

[shell]
allowed_commands = ["echo", "cargo", "git"]
denied_commands = ["rm", "curl"]
denied_patterns = ["sudo .*"]
max_output_bytes = 65536

[permissions]
default_mode = "ask"
[permissions.tools]
castellan_propose_mutation = "deny"
castellan_deposit_signal = "allow"

[[hooks.hooks]]
event = "PreToolUse"
command = "./scripts/hooks/pre-tool.sh"

Sections

[memory]

KeyTypeDescription
backendjson | sqliteDurable store backend (default: json)
pathstringSQLite database path when backend = "sqlite" (default: .castellan/castellan.db)
mirror_jsonboolAlso write .castellan/episodes/*.json alongside SQL (default: true)
auto_importboolImport existing JSON corpus on first SQLite open (default: true)
checkpoint_interval_ticksintegerPersist mid-episode checkpoints every N scheduler ticks (default: 0 = episode-end only)

See Memory architecture.

[governance]

KeyTypeDescription
session_spend_cap_usdfloatSession spend ceiling
max_risk_scorefloatRisk score threshold
denied_pathsstring[]Paths blocked for tool/file access
[governance.plan_verify]tableOpt-in Guardians-shaped prove-before-execute (default off)

[governance.plan_verify]

KeyTypeDescription
enabledboolRequire verified workflow_plan for high-risk tools (default false)
high_risk_toolsstring[]Tools that need a certificate (default shell, exec)
allowlisted_toolsstring[]Tools permitted inside a workflow plan
taint_edgesarrayOptional { source_ref_prefix, forbidden_sink_tools } constraints
policy_pathstringOptional TOML file overriding allowlists / taint

See Agent hooks — prove-before-execute.

[shell]

KeyTypeDescription
allowed_commandsstring[]Allowlist for shell plugin
denied_commandsstring[]Blocked command names
denied_patternsstring[]Regex patterns to block
max_output_bytesintegerCap captured stdout/stderr

[permissions]

KeyTypeDescription
presetcautious | balanced | permissiveApproval preset expanded into a base policy
default_modeallow | ask | denyDefault MCP tool policy (overrides preset base)
[permissions.tools]mapPer-tool overrides

Permission modes filter tools/list before exposure to MCP clients. castellan doctor reports the active preset.

[coevolution]

Fast-slow co-evolution flywheel. See Evolution.

KeyTypeDescription
fast_remediationboolDeposit pheromone hints from failed episodes after each run (default: true)
slow_evolve_everyintegerRun in-tree castellan evolve every N episodes (0 = off, default)
slow_evolve_generationsintegerGenerations per slow evolve pass (default: 2)

[models]

KeyTypeDescription
providerstringOptional LLM provider: openrouter, openai, ollama, or openai_compatible
base_urlstringOpenAI-compatible API root; defaults by provider (openrouterhttps://openrouter.ai/api/v1)
http_refererstringOptional HTTP-Referer header (OpenRouter attribution; Castellan default when provider = "openrouter")
x_titlestringOptional X-Title header (OpenRouter attribution; default Castellan)
defaultstringFallback model id for all roles (default: mock)
verifystringModel for the verify client
actstringModel for the act client
planstringModel for the plan client

Per-role tiers resolve through one OpenAI-compatible client factory (scheduler never routes through an LLM). Auth is env-only — never put API keys in TOML:

ProviderAPI key env (first wins)
openrouterOPENROUTER_API_KEY, then CASTELLAN_LLM_API_KEY, then OPENAI_API_KEY
openai / openai_compatibleOPENAI_API_KEY or CASTELLAN_LLM_API_KEY
ollamaoptional (CASTELLAN_LLM_API_KEY / OPENAI_API_KEY); local default URL from OLLAMA_URL + /v1
[models]
provider = "openrouter"
default = "openai/gpt-4o-mini"
verify = "anthropic/claude-sonnet-4"
act = "openai/gpt-4o"
# optional overrides:
# base_url = "https://openrouter.ai/api/v1"
# http_referer = "https://github.com/Alphabetsoup16/Flock"
# x_title = "Castellan"

--model / --model-tier on castellan run still override; rebuild CLI with --features llm-http for live HTTP providers.

[[recipes]]

KeyTypeDescription
namestringRecipe identifier for castellan recipes run <name>
descriptionstringShown by castellan recipes list
commandstringShell command executed via sh -c
cwdstringOptional working directory

Recipes inherit [shell] denied_commands / denied_patterns — they are not a policy bypass. Later config layers override recipes by name.

[[recipes]]
name = "gridworld-smoke"
description = "Run gridworld reach-target and write an episode JSON"
command = "castellan run --goal 'reach target' --plugin gridworld --json"

[[hooks.hooks]]

KeyTypeDescription
eventstringHook event name
commandstringShell command to invoke

See Agent hooks.

Environment overrides

VariableMaps to
CASTELLAN_SESSION_SPEND_CAP_USDgovernance.session_spend_cap_usd
CASTELLAN_MAX_RISK_SCOREgovernance.max_risk_score
CASTELLAN_SHELL_ALLOWEDCSV → shell.allowed_commands
CASTELLAN_SHELL_DENIEDCSV → shell.denied_commands
CASTELLAN_DENIED_PATHSCSV → governance.denied_paths
CASTELLAN_PERMISSION_MODEpermissions.default_mode
CASTELLAN_PERMISSION_PROMPTHeadless Prompt resolution: allow | deny (default deny when non-TTY). Legacy shim: FLOCK_PERMISSION_PROMPT
CASTELLAN_MEMORY_BACKENDmemory.backend
CASTELLAN_MEMORY_DBmemory.path
CASTELLAN_MEMORY_MIRROR_JSONmemory.mirror_json

Full list: Environment variables.

MCP tools

Castellan exposes a progressive-disclosure MCP surface: meta-tools for discovery, compact listings in tools/list, and full schemas on demand via castellan_describe_tool.

Registry: crates/castellan-mcp/src/registry.rs
Stdio server: castellan mcp
Live episodes: castellan run --mcpCastellanEngine::call_mcp_tool
External instrument: While any scheduler run is active, castellan mcp stdio clients can deposit into the live field via .castellan/live/ (no multiplexer join required). castellan_topology_snapshot returns the published tick + topology from the active session when present.

Live-run instrument

When castellan run is active, the engine publishes a session snapshot under .castellan/live/ each scheduler tick. External MCP clients (Cursor, scripts, another agent) call castellan_deposit_signal through castellan mcp stdio — deposits append to a JSONL queue and are drained into the live PheromoneField on the next tick. Episode JSON records external_mcp_deposit events.

# Terminal A — active run
castellan run --plugin gridworld --goal "reach target"

# Terminal B — external deposit (routes to live session when active.json exists)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"castellan_deposit_signal","arguments":{"zone":"grid","signal":"coordination","amount":5.0}}}' | castellan mcp

# Topology reflects live tick state
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"castellan_topology_snapshot","arguments":{}}}' | castellan mcp

Demo: bash docs/demo/mcp-live-instrument.sh

Discovery flow

flowchart LR
    A[Agent starts] --> B[discover / search tools]
    B --> C[describe_tool]
    C --> D[Invoke tool]
    D --> E[Governance + trace]

    classDef runtime stroke:#0969da,stroke-width:2px
    classDef substrate stroke:#5b6b8c,stroke-width:2px
    classDef gate stroke:#1a7f37,stroke-width:2px

    class A,B runtime
    class C,D substrate
    class E gate
  1. Discovercastellan_discover_tools returns domain-indexed tool cards (no full schemas).
  2. Searchcastellan_search_tools with query (and optional domain) narrows by intent. Results are BM25-ranked over tool name, summary, description, and domain; when no whole term matches (partial words), substring matching kicks in. The response reports which via "ranking": "bm25" | "substring".
  3. Describecastellan_describe_tool with name returns full schema, preconditions, side effects, examples.
  4. Invoketools/call or live mcp_tools observation payload.

Meta-tools are always listed with full schemas. Operational tools in tools/list use compact cards (call castellan_describe_tool for parameters).

Error responses

Discovery tools return "ok": false with stable error codes. Stdio server maps to JSON-RPC -32601 (not found) / -32602 (invalid params). Search queries are capped at 256 characters.

Meta tools

ToolDomainPurpose
castellan_discover_toolsmetaBrowse domains and tool names
castellan_search_toolsmetaBM25-ranked search over catalog
castellan_describe_toolmetaFull ToolSpec for one tool

Operational tools

ToolDomainSummaryLive (--mcp)Governance
castellan_deposit_signalsubstrateDeposit pheromone into zoneYespre/post hooks
castellan_read_signalsubstrateRead signal strengthYesread-only
castellan_read_blackboardsubstrateRead blackboard slotYesread-only
castellan_topology_snapshotsubstrateHarness topology JSONYesread-only
castellan_metricssubstrateSession countersYesread-only
castellan_multiplexer_statusmultiplexerDashboard statusline snapshotYesread-only
castellan_propose_mutationevolvePreview harness mutation (diff + drift); does not applyYespreview-only
castellan_resolve_mutationevolveAccept or reject a persisted proposalYeshigh-impact gate
castellan_read_resourcesubstrateResolve typed URI (episode://, genome://, pane://, blackboard://)Yesread-only
castellan_recallmemoryRecall durable memory by keyYesread-only
castellan_remembermemoryStore durable memory entryYespre/post hooks
castellan_query_field_historysubstrateQuery pressure-field historyYesread-only

All operational tools are in LIVE_TOOL_NAMES for castellan run --mcp.

Start the server

castellan mcp

Wire your MCP client to stdin/stdout JSON-RPC. Cursor config example in Cursor setup.

Example: discovery sequence

{"name": "castellan_search_tools", "arguments": {"query": "deposit", "domain": "substrate"}}
{"name": "castellan_describe_tool", "arguments": {"name": "castellan_deposit_signal"}}
{"name": "castellan_deposit_signal", "arguments": {"zone": "grid", "signal": "coordination", "amount": 2.0}}

Example: live episode

Observation payload for castellan run --goal "..." --plugin gridworld --mcp:

{
  "mcp_tools": [
    {
      "tool": "castellan_deposit_signal",
      "args": { "zone": "grid", "signal": "coordination", "amount": 2.0 }
    }
  ]
}

Trace JSONL emits mcp_discovery for meta-tools and mcp_tool for operational calls. Governance pre/post hooks apply on the hot path.

Design principles

PrincipleImplementation
Progressive discoveryMeta-tools before full schemas
Context efficiencyCompact tools/list for operational tools
Live paritySame registry for castellan mcp and castellan run --mcp
Description qualitySummary + preconditions + side effects per tool

Adding a tool

  1. Add ToolSpec in registry.rs and register in all_tool_specs().
  2. Add handler in handlers.rs and route in call_tool_with_registry.
  3. Add name to LIVE_TOOL_NAMES if live episodes should expose it.
  4. Update this page and run cargo test -p castellan-runtime.

See also MCP overview and Agent guide.

Socket API

Castellan exposes a newline-delimited JSON (NDJSON) API over a Unix domain socket. The in-tree castellan-multiplexer server implements Castellan mux automation; castellan-herdr is the NDJSON client crate (historical name).

Source: crates/castellan-multiplexer/src/api.rs · no external mux binary required

Connect

SettingDefault
Socket path$CASTELLAN_SOCKET~/.config/castellan/castellan.sock$XDG_RUNTIME_DIR/castellan.sock
ProtocolOne JSON request per line; one JSON response per line
Streamingevents.subscribe and pane.attach may stream additional NDJSON events

Ensure the server is running:

castellan multiplexer ensure
# or
castellan herd server

Example request:

{"id": 1, "method": "ping", "params": {}}

Example response:

{"id": 1, "result": {"type": "pong", "version": "...", "protocol": "..."}}

Errors use {"id": ..., "error": {"code": "...", "message": "..."}}.

Method catalog

MethodPurpose
pingHealth check; returns server and protocol version
session.snapshotFull session tree snapshot
session.attachResolve named session → workspace + pane
workspace.createCreate workspace with label, optional cwd
workspace.listList workspaces
tab.createCreate tab in workspace
tab.listList tabs (optional workspace filter)
tab.focusFocus tab by id
pane.splitBSP split (direction, ratio, optional cwd)
pane.swapSwap panes
pane.zoomZoom pane (mode: toggle / in / out)
pane.focusFocus pane
pane.listList panes (optional workspace/tab filter)
pane.readRead visible screen text
pane.send_textSend text to pane PTY
pane.send_keysSend key sequence
pane.send_inputSend text and/or keys
pane.resizeResize PTY rows/cols
pane.writeRaw write to pane
pane.attachStream pane output (subscription)
pane.report_agentHook: report agent name + state
pane.clear_agent_authorityClear agent authority on pane
agent.getAgent info for target pane
agent.sendSend text to agent pane
agent.listList agents (optional workspace filter)
agent.startStart registered agent in new/split pane
agent.explainDiagnose agent state heuristics
events.subscribeStream filtered events
events.emitPublish an event onto the server EventBus (used by castellan run / ACP when the socket is healthy)
events.waitBlock until matching event (timeout)
worktree.createGit worktree helper
worktree.listList worktrees
worktree.removeRemove worktree
server.live_handoffGraceful server restart (layout preserved)
server.stopAcknowledge stop (server may exit)

Methods marked Stream return an initial result then additional NDJSON event lines on the same connection.

CLI mapping

Automation needCLI
Rich status dashboardcastellan herd status
Attach to panecastellan herd attach --pane <id>
Wait for agent idlecastellan herd wait --pane <id> --status idle
Install agent hookscastellan integration install <agent>
Remote over SSHcastellan remote --ssh user@host

Agent detection

Screen heuristics detect Claude, Codex, and Cursor agent states (idle / working / blocked). Hooks via pane.report_agent provide authoritative state when integrations are installed.

Live handoff

server.live_handoff writes ~/.config/castellan/sessions/handoff.json, spawns a replacement daemon, and releases the socket. PTY processes are not preserved across handoff — layout and visible text restore only.

Testing

cargo test -p castellan-multiplexer
cargo test -p castellan-herdr
castellan multiplexer ensure
castellan herd status

See Daily driver loop for the operator workflow.

Environment variables

VariableDefaultPurpose
CASTELLAN_SOCKET~/.config/castellan/castellan.sockMultiplexer Unix socket path
CASTELLAN_ENVunsetSet to 1 in castellan-managed pane shells
RUST_LOGinfoTracing filter for castellan subcommands
CASTELLAN_DOCS_URLhttps://castellan-docs.pages.devllms.txt / sitemap base URL in CI scripts
OPENROUTER_API_KEYunsetOpenRouter auth when [models] provider = "openrouter" (never store in TOML)
OPENAI_API_KEYunsetOpenAI / compatible HTTP LLM auth
CASTELLAN_LLM_API_KEYunsetFallback LLM API key (any OpenAI-compatible provider)
CASTELLAN_LLM_BASE_URLprovider defaultOverride OpenAI-compatible base URL
CASTELLAN_LLM_MODELgpt-4o-miniDefault model id for env-based HTTP clients
OLLAMA_URLunsetOptional local LLM base (doctor/tests; Ollama provider appends /v1)
OLLAMA_MODELllama3.2Model name for Ollama chat API
GOVCRAFT_MODEin_tree_honest_equivalentMeeting-sched acceptance mode: upstream_meeting_sched, llm_scheduling_hook (historical env name)
GOVCRAFT_UPSTREAM_TRIALS5Trial count for --upstream-repro deterministic sweep
CASTELLAN_PERMISSION_MODEunsetOverride permissions.default_mode (allow / deny / prompt)
CASTELLAN_PERMISSION_PROMPTunset (non-TTY → deny)Headless Prompt resolution: allow or deny. TTY castellan run prompts interactively when unset. Legacy: FLOCK_PERMISSION_PROMPT

OpenRouter

export OPENROUTER_API_KEY=sk-or-...
# castellan.toml:
# [models]
# provider = "openrouter"
# default = "openai/gpt-4o-mini"
cargo run -p castellan-cli --features llm-http -- doctor
cargo test -p castellan-runtime --features llm-http openrouter_chat_completions_smoke -- --ignored

castellan doctor stage llm_provider reports key_present and TCP reachability — never the key value.

Upstream meeting-sched acceptance (Bet 1)

Optional LLM repro on the upstream meeting-sched fixture (govcraft_acceptance crate — historical name):

export OLLAMA_URL=http://127.0.0.1:11434
cargo test -p govcraft_acceptance --test llm_integration -- --ignored
cargo run -q -p govcraft_acceptance -- --upstream-repro

Without OLLAMA_URL, --upstream-repro emits deterministic baselines and an honest gap table (not the published 48.5% LLM band).

Session / remote

VariablePurpose
SSH_AUTH_SOCKUsed by castellan remote --ssh for agent forwarding

Hooks

See agent hooks for CASTELLAN_SOCKET in shell hook scripts.

Engineering guide

Rust conventions for Castellan contributors. Guardrail enforcement lives in governance policies.

Toolchain

  • Pin via rust-toolchain.toml
  • Format: cargo fmt --all
  • Lint: cargo clippy --workspace --all-targets -- -D warnings

Verification

TierCommand
Iterate./scripts/verify.sh --fast
Before PR / CI./scripts/verify.sh (full)
./scripts/verify.sh --fast
./scripts/verify.sh
castellan drift-check

Details: Verify before PR. Operators who only run Castellan day-to-day should follow Daily driver instead — that path uses castellan drift-check, not ./scripts/verify.sh.

Shared Cargo target (worktrees)

Optional helper for multiple worktrees:

source scripts/dev-env.sh   # exports CARGO_TARGET_DIR to a shared directory

verify.sh defaults to $ROOT/target for isolation unless CARGO_TARGET_DIR is already set.

Crate boundaries

CrateResponsibility
castellan-coreTypes: Goal, EpisodeLog, topology
castellan-runtimeScheduler, engine, thin MCP host glue
castellan-eventsUnified observability schema
castellan-cliOperator surface
castellan-multiplexerPTY + socket API

Docs

  • Published book (book/src/) is canonical for operators — castellan-docs.pages.dev
  • docs/ holds engineering specs; see docs/INDEX.md
  • Do not link raw GitHub docs/ blobs from book pages — use relative book links

Agent workflows

PathWhen
Frontier sprintBacklog-driven BL delivery; Full vs Fast path + verify_tier
PrewalkMulti-file work: frontier explores, capped todos, first edit, then cheap executor

Verify before PR

Contributor gate for Castellan development. Operators running agents day-to-day should use Daily driver (castellan drift-check) instead of this script.

The single verification entry point is ./scripts/verify.sh:

TierCommandUse when
Fast./scripts/verify.sh --fastLocal iteration / residuals — fmt + cargo test --workspace + smoke-docs
Full./scripts/verify.sh or --fullBefore every PR; CI default — clippy, hack, release build, all smokes + moat proofs

Full tier always retains moat proofs: ablation decay, Φ/convergence gate, mid-run topology drain, Guardians plan_verify. Long orchestration demos (rah/swarm/mux flywheel, field/pane demos) are full-only.

./scripts/verify.sh --fast    # while coding
./scripts/verify.sh           # before PR
castellan drift-check

CI runs the full script on every PR (.github/workflows/ci.yml). Shared CARGO_TARGET_DIR across worktrees: optional source scripts/dev-env.sh (see engineering).

Documentation style

Guidelines for Castellan book pages.

Voice

  • Honest scope — say what Castellan is not early
  • Operator-first — copy-paste commands that produce visible artifacts
  • No placeholder social proof
  • Lexicon: organism + coordination field (genome, vitals, immune, stigmergy, pressure, blackboard, mux panes, dashboard). No animal pack metaphors (herd/flock/rookery) and no feudal/military castle-staff metaphors in body copy. Canonical table: Conventions.
  • CLI names: keep castellan herd / swarm-demo as commands; describe them as mux UX / multi-pane demos in prose
  • Peer names: no “like X / parity with Y” framing on operator pages; landscape peers stay in Frontier + COMPETITIVE_MATRIX / VENDOR
  • Structure: What → Why → How → Failure paths → Next; cross-link instead of repeating essays

Formatting

  • One H1 per page (mdbook enforces)
  • Use > **Note** / > **Warning** admonitions
  • Mermaid diagrams: use classDef + semantic classes (runtime, substrate, evolve, gate) — see Design system
  • No inline style fill:#... (breaks dark mode)
  • Max 3 columns in comparison tables
  • Relative book links only — never link to raw GitHub docs/ blobs from book pages
  • Cross-link concepts ↔ reference ↔ integrations

Brand tokens

Hero line (keep in sync with DESIGN.md): Pressure wakes agents — multiplexer, dashboard, and episode flywheel in one binary.

See Design system for the full token reference, typography scale, layout, components, and Mermaid rules.

Quick palette (defined in book/theme/css/custom.css):

  • --castellan-coordination — teal links and active nav
  • --castellan-evolve — amber evolution accents
  • --castellan-runtime / --castellan-substrate / --castellan-gate — diagram roles

Harness capability docs

  • Single source: docs/harness-manifest.toml drives docs/HARNESS_FEATURES.md and the landscape table in docs/COMPETITIVE_MATRIX.md.
  • Update workflow: edit the manifest, then cargo run -p harness-docs -- generate.
  • CI: ./scripts/check-harness-doc-parity.sh (via verify.sh smoke-docs) fails on drift.

Deployment

Audience: maintainers publishing docs and configuring CI.

Documentation site (mdbook → Cloudflare Pages)

Castellan docs are built with mdbook from book/ and deployed on every push to main that touches book/, docs/, or .github/workflows/docs.yml.

EnvironmentURL
Productionhttps://castellan-docs.pages.dev
Cloudflare Pageshttps://castellan-docs.pages.dev

Local build

./scripts/build-docs-site.sh   # canonical — installs mdbook, mermaid, llms.txt, sitemap
# or manually:
cargo install mdbook mdbook-mermaid --locked   # once
mdbook build book
open book/book/index.html

Live preview:

mdbook serve book --open

./scripts/verify.sh runs scripts/smoke-docs.sh, which includes mdbook build book.

CLI help snapshots

When CLI flags or subcommands change intentionally:

UPDATE_SNAPSHOTS=1 cargo test -p castellan-cli cli_help_snapshot

verify.sh runs cli_help_snapshot tests (BL-014). MCP tool names must appear in MCP tools — enforced by scripts/check-mcp-doc-parity.sh (BL-015).

CI workflow

Workflow: .github/workflows/docs.yml

  1. Install Rust toolchain and mdbook
  2. ./scripts/build-docs-site.sh → output in book/book/
  3. cloudflare/wrangler-action uploads book/book/ to Pages project castellan-docs

Model A (canonical): GitHub Actions direct upload — not Cloudflare Git integration. Keep CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in GitHub secrets.

gh workflow run docs.yml   # manual deploy

Cloudflare Workers Builds (dashboard)

Production health = GitHub Actions → Deploy Docs, not the Cloudflare Workers Builds list. If the dashboard is red with “build token … deleted or rolled” while https://castellan-docs.pages.dev returns 200 and Deploy Docs is green, production is fine — disconnect Model B.

SymptomCauseFix (operator)
Build token deleted/rolledStale dashboard Git / Workers Builds tokenDisconnect Git: Dashboard → Workers & Pages → castellan-docs → Settings → Builds → Disconnect, or run Actions → Disconnect CF Workers Builds
Build command failsDashboard Git expects a build we do not useSame — disconnect; rely on Model A (docs.yml + wrangler-action)
Wrong build commandDashboard runs incomplete buildDo not “fix” Model B; keep GitHub Actions as the only publisher

Automated in-repo: scripts/build-docs-site.sh, .github/workflows/docs.yml (Model A), .github/workflows/cf-builds-disconnect.yml (silence Model B). Root wrangler.toml is optional local assets config — it must not be paired with a connected Git repo in the dashboard.

Operator checklist:

  1. Confirm CLOUDFLARE_API_TOKEN has Account → Cloudflare Pages → Edit (Model A)
  2. Confirm CLOUDFLARE_ACCOUNT_ID is the 32-char account ID (sidebar), not a zone ID
  3. Disconnect Workers Builds Git so only docs.yml deploys (workflow above, or dashboard Disconnect)
  4. Re-create the GitHub API token if docs.yml deploy returns HTTP 403 — do not confuse that with the dashboard build token

Required GitHub secrets

SecretDescription
CLOUDFLARE_API_TOKENAccount Settings Read + Cloudflare Pages Edit
CLOUDFLARE_ACCOUNT_ID32-char account ID from dashboard sidebar — not a zone ID

Troubleshooting

SymptomFix
Deploy 403Token lacks Pages Edit for target account
Wrong account IDCopy from Workers & Pages sidebar, not zone overview
Dashboard build token errorDisconnect Git builds; use Model A CI upload only; see Workers Builds table above

Application / CLI releases

The Castellan CLI is not yet published to crates.io (API still moving). Install from source:

cargo install --path crates/castellan-cli --locked

Semver tags and CI

StepCommand / workflow
Local verify./scripts/verify.sh
Release prep (dry-run publish)./scripts/release-prep.sh [version]
Tag pushgit tag -a v0.1.0 -m "Release v0.1.0" && git push origin v0.1.0
GitHub Release.github/workflows/release.yml — verify, artifact upload, changelog body

MSRV: rust-version = "1.75" in root Cargo.toml. Release workflow pins toolchain 1.75.

Changelog: CHANGELOG.md (Keep a Changelog format).

CI runs ./scripts/verify.sh on every PR and push to main.

Conventions

One page freezes the vocabulary. If a term isn’t here, it’s defined where it’s used.

Canonical lexicon

Castellan docs use two metaphor families — organism (biology of a harness) and field (coordination through the environment). Product name Castellan and Castle Holdings branding stay. Body copy does not lean on animal packs or feudal/military castle staff.

PreferAvoidMeaning
Organism / harness as living systemStatic config file as the whole storyGenome + circulation + immune gate over a run
GenomeOpaque “config blob” without lineageNamed topology snapshot in the evolve archive (parent_genome_id)
Circulation / vitalsVague “health metrics” without the CLITick-level field health (castellan vitals, CirculationVitals)
Immune gate / quarantine“Auto-merge whatever evolve proposes”Reject or quarantine harmful topology mutations before seeding
Evolve / episode flywheel“Retrain the model”run → episodes → evolvedrift-check
EpisodeVague “session log”One castellan run goal attempt under .castellan/episodes/
StigmergyAgent-to-agent chat / router LLM handoffsCoordination through deposits in the shared environment
Pheromone / depositsChat messages between agentsDecaying zone signals agents write; scheduler reads
Pressure (field / scheduler)Manager model picking the next agentWake priority from deps + zone signals
BlackboardFree-form shared scratchpad chatTyped JSON slots plugins declare
Mux panes / multiplexerBare tmux-script farm metaphorsIn-tree PTY workspaces, tabs, panes
DashboardControl tower / war roomRatatui operator UI (castellan dashboard)
Agents / panes / coordination fieldHerd, flock, rookery, swarm-as-animals, “coordinate the herd”Who runs and where they deposit
Plain operator languageCastellan-as-officer, garrison, troops, keep/lord body metaphorsHow humans drive the binary

Command names vs prose

castellan herd and castellan swarm-demo remain CLI subcommand names (historical product surface). In prose, describe what they do — multiplexer UX, multi-pane demo — without animal metaphor. Prefer saying “mux status” / “pane status” next to the literal command:

castellan herd status   # pane / mux status (subcommand name is historical)

Crate names (castellan-herdr) and hook scripts (herdr-agent-state.sh) stay as identifiers. Peer product names do not appear in operator prose — see Doc style.

Brand vs body copy

KeepDo not expand into body metaphor
Product name CastellanGarrison, troops, lord of the keep
Logo: castle keep silhouette (theme asset)Feudal / military narrative in guides
Castle Holdings (org branding)“Coordinate the herd / flock / rookery”

Names and paths

TermValueNotes
BinarycastellanSingle CLI; subcommands below
Config filecastellan.tomlLayered — repo root, then .castellan/, then env
State directory.castellan/Episodes, archive, proposals, skills
Episode logs.castellan/episodes/One JSON per goal run (castellan run)
Topology archive.castellan/topology_archive.jsonGenome lineage for castellan evolve / --seed-archive
Multiplexer socket~/.config/castellan/castellan.sockDefault Unix socket; override with --socket or CASTELLAN_SOCKET
Statusline snapshot~/.config/castellan/statusline.jsonWritten by castellan dashboard; read by castellan_read_statusline
Telemetry DB~/.config/castellan/telemetry.dbcastellan telemetry query target
MCP toolscastellan_* prefixe.g. castellan_deposit_signal, castellan_discover_tools — see MCP tools
Permission PromptTTY ask / non-TTY fail-closedPermissionMode::Prompt uses a host prompter; see Governance overview and CASTELLAN_PERMISSION_PROMPT

Note: Paths above are defaults. Named multiplexer sessions (--session) scope their own socket and persist directory — see Multiplexer overview.

Glossary

TermOne-liner
EpisodeA single castellan run goal attempt, logged as JSON under .castellan/episodes/ — field state, topology, wake stats, verdict
TopologyThe wake graph (nodes, edges, thresholds) agents coordinate through; mutated by castellan evolve
Coordination fieldShared environment: blackboard + pheromone zones + pressure scheduler
Pressure fieldScheduler wake-priority substrate — pheromone zones + dependency signals decide when a pending task dispatches
StigmergyCoordination by depositing into the environment instead of negotiating in chat
Pheromone / depositNamed zone signal with decay; agents write, scheduler and peers read within perception radius
BlackboardTyped JSON slots agents read/write instead of talking to each other in natural language
GenomeA named topology snapshot in the evolve archive, with lineage (parent_genome_id)
Circulation / vitalsPer-run field health (castellan vitals) — cold zones, fever, expression
Immune gateEvolve-time reject/quarantine of harmful topology candidates
QuorumMulti-contributor density gate for colony phase unlock (not a router LLM)
Response thresholdPer-node θ for task kinds; surplus drives emergent specialization
Corridor / PhysarumOptional trail edges with evaporation + conductance pruning
MAP-ElitesIlluminated genome archive by behavior descriptor cells
Drift-checkcastellan drift-check — validates repo guardrails and topology mutations against the verification manifest
RAH (Recursive Agent Harness)Depth-bounded harness-within-harness spawn (multiplexer pane + topology binding); see castellan rah-demo and Frontier positioning
Mux / multiplexerIn-tree PTY server and pane tree; CLI UX includes historical castellan herd

Page structure (Q&A order)

Prefer this order on concept and operate pages so readers get answers without hunting:

  1. What — one-sentence definition
  2. Why — the bet / what it replaces
  3. How — commands and mechanism
  4. Failure paths — symptom → cause → fix
  5. Next — cross-links, not a second essay

Cut redundancy: link Daily driver / Coordination model / Organism model instead of repeating them.

Command surface at a glance

castellan doctor
castellan --version
castellan --help

Frontier positioning

Thesis: Castellan owns stigmergic runtime coordination, verified harness recursion (RAH), and episode-grounded topology evolution — not meta-harness wrapping or workflow DSLs.

What Castellan owns

WedgeEvidence
Pressure-field schedulercastellan run, stigmergy benches in verify.sh
In-tree multiplexerNDJSON socket API, no external mux binary
Topology evolutioncastellan evolve, .castellan/topology_archive.json
Honest verificationcastellan drift-check, plugin-grounded fitness

What Castellan is not competing on

Use Castellan when coordination geometry must evolve from episode logs — not when you need a drop-in single-agent coding product. Scope detail: What Castellan is / is not.

Maintainer landscape (peer names quarantined here and in docs/COMPETITIVE_MATRIX.md / docs/FRONTIER_POSITIONING.md — not sprinkled through concept pages).

Stigmergy ablation

Method: In-tree honest proxies + upstream meeting-sched fixture. LLM repro optional via OLLAMA_URL.

Summary

BenchStigmergy / pressure-fieldRandom / conversationNotes
Chain dispatch (stigmergy_ablation)5/5 zones0/5 zonesDependency wake via pheromone deposits
Gridworld success rate (8 runs)100%100%Both configs reach target; chain dispatch is the discriminant
Meeting-sched proxy (govcraft_acceptance)41.7% booking rate8.3% booking rateDiscrete slot scheduling with PheromoneField
Upstream fixturedeterministic PF / convfixtures/upstream_easy_seed4242.json
Published LLM reference48.5% (reference)12.6% (reference)Honest gap unless OLLAMA_URL repro lands in band

Honest gap table (Bet 1)

MetricMeasured (in-tree)Published reference
Upstream task shape pressure-field (deterministic)~80% solve (5 trials, seed 4242–4246)48.5% LLM
Upstream task shape conversation (deterministic)~100% solve (same trials)12.6% LLM
Slot proxy stigmergy (decay on)~41.7% booking rate
Slot proxy stigmergy (decay off)~8.3% booking rate
Slot proxy random8.3% booking rate

Decay ablation: Sequential within-round booking used to mask decay (always filled all 5 slots). Fixed with parallel picks + conflict resolution: decay off collapses to one slot (~8%); decay on spreads (~42%). cargo run -q -p govcraft_acceptance -- --ablation validates ≥5% gap.

Upstream fixture

cargo run -q -p govcraft_acceptance -- --mode upstream_meeting_sched
cargo run -q -p govcraft_acceptance -- --upstream-repro

Fixture: ScheduleGeneratorConfig::easy(), seed 4242, at benches/govcraft_acceptance/fixtures/upstream_easy_seed4242.json.

Honest labeling

  • In-tree proxy: benches/govcraft_acceptance and benches/stigmergy_ablation use Castellan PheromoneField + scheduler semantics.
  • Upstream fixture: rooms, attendees, duration slots, overlap predicate (distinct from slot proxy).
  • Not reproduced (LLM): 48.5% vs 12.6% unless --upstream-repro with OLLAMA_URL measures in-band rates.
  • CI: Slot proxy + ablation in ./scripts/verify.sh; upstream mode opt-in.

Provenance

Upstream meeting-sched task shape and published LLM band cite pressure-field-experiment. Crate/binary names (govcraft_acceptance, GOVCRAFT_*) are historical identifiers.

Reproduce

cargo run -q -p stigmergy_ablation
cargo run -q -p govcraft_acceptance
cargo run -q -p govcraft_acceptance -- --mode upstream_meeting_sched
bash docs/demo/run-ablation.sh
./scripts/verify.sh