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

Self-evolving agent coordination runtime — the harness is the castellan.

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

Terminal demo — run → episode → multiplexer

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

Download castellan-demo.cast · Run live from repo root: ./docs/demo/castellan-demo.sh

Castellan is a Rust coordination runtime: agents wake from a decaying pressure field, verify work through typed harness recursion (RAH), and improve topology from episode logs.

What makes Castellan different

DimensionTypical harnessCastellan
CoordinationScripts / chatStigmergic pressure field + scheduler
TopologyFixedEvolved from .castellan/episodes/
RecursionAPI subagentsRAH verified depth in multiplexer panes
ExecutionVendor PTYIn-tree multiplexer + castellan dashboard

Castellan is not a meta-harness wrapper or workflow DSL. Default castellan run uses deterministic plugins (gridworld, shell) — wire your LLM via MCP, ACP, or remote panes. See What Castellan is / is not.

Conventions

TermMeaning
Binarycastellan — single CLI entrypoint
ConfigLayered castellan.tomlcastellan.toml reference
Workspace.castellan/ — episodes, archive, traces
MCP toolscastellan_* prefix — MCP tools
Socket APINDJSON on multiplexer socket — Socket API

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 or castellan run --rlm
  2. Log — episode JSON in .castellan/episodes/
  3. Evolve — topology mutations from corpus
  4. Gatecastellan drift-check manifest guardrails
castellan run --goal "reach target" --plugin gridworld

Full walkthrough: Quickstart.

Quick paths

GoalPage
First episodeQuickstart
Daily driver loopDaily driver
Agent in panesAgent guide
CLI surfaceCLI reference

Common commands

castellan run --goal "reach target" --plugin gridworld
castellan --help
castellan evolve --episodes 16 --workspace .castellan/episodes
castellan drift-check

Build this book locally

mdbook build book && mdbook serve book

See Deployment. Live site: castellan-docs.pages.dev.

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 episode walkthroughQuickstart
Multiplexer daily loopDaily driver
Agent onboardingAgent guide

Quickstart

Clone → gridworld episode in under two minutes.

Install

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

From source: Install.

First success

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

Episode JSON lands in .castellan/episodes/.

Terminal demo

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

Download castellan-demo.cast

Common commands

castellan --help
castellan run --goal "reach target" --plugin gridworld --rlm
castellan evolve --episodes 16 --workspace .castellan/episodes
castellan replay --episode .castellan/episodes/<goal-id>.json
castellan multiplexer ensure && castellan dashboard
castellan herd status
castellan drift-check
castellan mcp

Next steps

GoalPage
Daily driver loopDaily driver
MCP tool catalogMCP tools
Remote multiplexerCLI — remote
Verify before PRVerify
./scripts/verify.sh

Daily driver loop

The recommended local workflow for developing and operating Castellan.

Loop

castellan multiplexer ensure  →  castellan dashboard  →  castellan run  →  castellan evolve  →  ./scripts/verify.sh
  1. Multiplexercastellan multiplexer ensure starts the in-tree PTY server (~/.config/castellan/castellan.sock).
  2. Dashboardcastellan dashboard opens the ratatui control tower (agents, pressure, pane tree). Writes ~/.config/castellan/statusline.json for MCP observability.
  3. Runcastellan run --goal "…" --plugin gridworld (scheduler) or --rlm (verify/act loop with mux pane topology).
  4. Evolvecastellan evolve --episodes 16 --workspace .castellan/episodes mutates topology from episode corpus.
  5. Verify./scripts/verify.sh before every PR (fmt, clippy, hack, tests, smoke benches, dashboard + socket e2e).

Headless CI parity

CI runs the same smoke surface without a TTY:

cargo test -p castellan-dashboard --test integration
cargo test -p castellan-multiplexer --test socket_integration
castellan run --goal "reach target" --plugin gridworld --rlm

Remote / SSH

castellan remote --goal "reach target" --plugin gridworld
castellan herd attach --session my-work

See Dashboard and Verify.

Dashboard

castellan dashboard is the ratatui control tower over the in-tree multiplexer — the daily-driver view of agents, pressure, pane topology, and evolution archive.

Launch

castellan multiplexer ensure
castellan dashboard

Defaults:

  • Socket: ~/.config/castellan/castellan.sock (override with --socket)
  • Episodes: .castellan/episodes
  • Archive: .castellan/archive

Keybindings

KeyAction
j / kSelect agent up/down
EnterAttach to selected pane (Ctrl+Q to detach)
rRefresh agents + layout snapshot
qQuit

Headless observability

The dashboard writes ~/.config/castellan/statusline.json on each refresh. MCP tool castellan_read_statusline (see MCP tools) reads this snapshot for external monitors.

CI smoke

Dashboard client code is tested against a mock Unix socket:

cargo test -p castellan-dashboard --test integration

Included in ./scripts/verify.sh.

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 — HerdR-shaped NDJSON socket API + PTY panes
  • 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

  • An OpenCode / Claude Code / claw-code clone — no drop-in coding agent with dozens of providers on the hot path
  • An OpenClaw wrapper — Castellan does not host or wrap external harness binaries; it is the coordination runtime
  • A meta-harness composition layer — unlike Omnigent-style wrappers, Castellan does not orchestrate Claude Code + Codex 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: claw-code and OpenClaw optimize single-agent coding loops. Castellan optimizes multi-agent coordination geometry — scheduler ticks, pheromone deposits, topology evolution, and multiplexer pane farms.

Comparison snapshot

NeedCastellanclaw-code / OpenClaw
Daily coding agentUse castellan acp or external editor + MCPPrimary product
Multi-pane agent farmIn-tree multiplexer + dashboardExternal tmux scripts
Topology evolutioncastellan evolve + archiveNot in scope
Stigmergy / pressure fieldCore schedulerNot in scope
Provider matrixOptional verify-path LLM onlyHot-path feature

Honest adoption path

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

Castellan deliberately does not chase OpenCode/Claude Code/claw-code feature parity — coordination runtime and honest scope are the product.

Verify

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

  • cargo fmt --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo hack check --workspace --each-feature --no-dev-deps
  • cargo test --workspace
  • Release build + CLI smoke tests (run, json, rah-demo, acp, swarm-demo)
  • Benchmarks: stigmergy ablation, govcraft acceptance, topology ablation
  • Documentation smoke: ./scripts/smoke-docs.sh
./scripts/verify.sh
castellan drift-check

CI runs the same script on every PR (.github/workflows/ci.yml).

Troubleshooting

Common first-run failures and fixes.

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

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

Coordination model

Agents coordinate through the environment — typed blackboard slots, decaying pheromone zones, and scheduler wake pressure. No conversation loops, router LLMs, or NL handoff chains.

Substrate primitives

PrimitiveRole
BlackboardTyped JSON slots; plugins read/write via PluginContext
Pheromone zonesSignal maps with exponential decay (PheromoneField)
Perception radiusBounds how far an agent reads field signals
Pressure fieldPressureScheduler queue — wake when deps + pressure ≥ threshold
CompletionNextAction::Complete deposits completion signal; dependents wake

Wake pressure = min(base_pressure, min(dep signals)), boosted by multiplexer deposits.

Allowed vs forbidden

PatternStatus
Deposit completion → dependent wakesAllowed
Blackboard slot handoff (typed JSON)Allowed
NextAction::Delegate { task }Allowed
Multiplexer pane depositsAllowed
Agent-to-agent chatForbidden
Router LLM choosing next agentForbidden
Re-encoding substrate as NLForbidden

Coordination flow

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:#8250df,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

Scheduler loop

CastellanEngine::run_goal:

  1. Encode initial task from plugin manifest
  2. Decay field → refresh pressure → dispatch ready batch
  3. Read host output → apply deposits → coordinate in zone
  4. Verify → deposit completion → enqueue retries/delegates
  5. Snapshot field, topology, wake stats into episode JSON

Multiplexer deposits

Remote observations may include pane deposits:

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

These feed herdr_wake_boost, waking stalled tasks when panes report activity.

See also

Memory architecture

Castellan memory is the coordination substrate plus durable episode artifacts — not a vector store or conversation archive.

Layers

LayerWhatWhere
HotBlackboard, PheromoneField, schedulerIn-process
WarmEpisodes, traces, checkpoints, topology archive.castellan/ JSON or SQLite
ColdTurso Sync (planned)Multi-machine share
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:#8250df,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

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

Today (JSON default)

castellan run --goal "reach target" --plugin gridworld
castellan replay --episode .castellan/episodes/<id>.json
castellan memory status
ArtifactPath
Episodes.castellan/episodes/<goal-id>.json
Traces.castellan/traces/<goal-id>.jsonl
Topology archive.castellan/topology_archive.json

Live episodes: castellan_read_blackboard, castellan_deposit_signalMCP tools.

SQLite backend

Set [memory] backend = "sqlite" in castellan.toml:

  • castellan_recall — query episodes, checkpoints, observations
  • castellan_remember — persist agent observations (not live field deposits)
  • castellan_query_field_history — sparse field snapshots at checkpoints

castellan evolve reads SQL aggregates when backend is sqlite. Full config: castellan.toml.

What never goes in the database

DataWhy
Live pheromone ticksIn-process decay semantics
Conversation transcriptsAnti-coordination — see Coordination model
Vector embeddingsOut of scope v1

Engineering detail

SQL schema, MemoryStore trait, sync policy: MEMORY_ARCHITECTURE.md (maintainer spec).

Organism model

Castellan treats the harness as a living organism: genomes express coordination parameters, circulation vitals track field health, and an immune gate rejects harmful topology mutations.

Status: Phases 1–4 complete (circulation, DNA lineage, immune gate, per-zone expression).

Architecture

castellan-core/organism.rs     DNA types + circulation vitals + zone expression
castellan-substrate/pheromone  per-zone decay (fallback to global lambda)
castellan-runtime/circulation  vitals + immune_fever tracking
castellan-runtime/engine.rs    vitals + genome_id + fever + decay wire-through
castellan-evolve/genome.rs     lineage + crossover + archive reconstruction
castellan-evolve/immune.rs     corpus threat detection + auto_reject
castellan-evolve/mutator.rs    crossover + cold-zone decay adjustment
castellan-cli/organism_cmd.rs  castellan vitals | castellan genome list|lineage

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. CastellanGenome expresses coordination + decay params at episode end
  2. PheromoneField circulation → CirculationVitals in episode
  3. castellan evolve mutates or crossover (Pareto ≥ 2) → immune_report() gates accept
  4. Quarantined genomes excluded from archive.nearest() seed
  5. Per-zone decay tuned from proposer cold-zone heatmap

CLI

castellan run --goal "reach target" --plugin gridworld
castellan vitals
castellan evolve --episodes 10
castellan genome list
castellan genome lineage <genome_id>

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)

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

Known deferred

  • flux-grid in swarm-demo default path
  • Full parent_ids lineage CLI (crossover sets parent_ids; CLI shows primary parent_genome_id only)
  • RAH_MAX_DEPTH in castellan-core (cycle avoidance with castellan-evolvecastellan-rah)

See also

Topology evolution

Frontier bet: evolution that changes coordination geometry, not prompts.

What this proves

  1. Episode logs from real castellan run / swarm-demo runs ground fitness via plugin_grounded_fitness().
  2. castellan evolve --episodes 10 runs accept/reject over generations (drift + immune gates).
  3. Drift gate: incompatible candidates are rejected on the production path; drift_rejected and drift_violations appear in CLI JSON when the gate fires.
  4. Gen-0 vs gen-N fitness delta and genome lineage are observable in CLI output.

Quick run

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

Or step by step:

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

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,
  "seed_source": "episodes",
  "pareto_front": []
}

CI gate

Fast smoke (no LLM, fixture episodes only):

cargo run -q -p evolve_proof

Fixture corpus: tests/fixtures/evolve-episodes/ (4 gridworld episodes).

Honest limits

  • Fitness is plugin-grounded from in-tree gridworld episodes, not Govcraft 48.5% LLM benchmark.
  • Mutations target wake threshold, topology nodes, and per-zone decay — not prompt strings.
  • Crossover activates when Pareto front has ≥2 non-quarantined entries.
  • Drift gate blocks structurally invalid / manifest-failing candidates before fitness accept.

Evolve → run round-trip

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

Overview

Castellan closes the loop between topology evolution and production runs:

  1. castellan evolve — mutates harness topology from episode corpus; writes .castellan/topology_archive.json
  2. castellan run --seed-archive — seeds topology/coordination from nearest archive entry; logs genome_id + genome_seed event
  3. Write-back — after a successful run, merges episode fitness into the archive entry (by genome_id)

Write-back is enabled with --write-back or automatically when --seed-archive is used. Pass --no-write-back to disable the automatic path.

Quick reproduce

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

Expected in episode JSON:

  • genome_id — seeded from archive
  • fitness — plugin-grounded success score
  • events[] containing genome_seed and (when write-back applies) archive_write_back

Write-back semantics

  • Trigger: GoalStatus::Satisfied and both genome_id + fitness present
  • Merge: TopologyArchive::write_back_fitness updates matching entry by id; re-sorts by success score
  • Archive path: --archive (default .castellan/topology_archive.json)

JSON events

With --json, archive write-back emits a typed archive_write_back CastellanEvent line before episode_end.

RLM loop

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

Sandbox seam (RlmSandboxHost)

Behind castellan-rlm feature sandbox — stub for future Modal/Daytona backends:

#![allow(unused)]
fn main() {
use castellan_rlm::{RlmSandboxHost, SandboxRequest, StubSandboxHost};
}

Not wired into run_until_satisfied yet.

Governance overview

Castellan ships two governance surfaces:

  1. Manifest drift-checkcastellan drift-check enforces required CI checks, style files, and deprecation notices. See policies.
  2. Runtime hookscastellan.toml session hooks and multiplexer agent lifecycle reporting. See hooks.

Neither surface uses a router LLM. Governance is deterministic policy + operator-configured shell hooks.

Quick commands

castellan drift-check          # manifest guardrails
castellan doctor --json        # staged runtime diagnostics

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.

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):

#!/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)

Multiplexer overview

Castellan ships an in-tree PTY multiplexer with a HerdR-shaped NDJSON socket API. One binary (castellan) serves daily-driver pane farms without an external herdr binary.

Architecture

castellan herd server  →  castellan-multiplexer (Unix socket)
        │
        ├── pane.spawn / pane.attach (PTY proxy)
        ├── events.subscribe / events.wait
        └── pane.report_agent (hook protocol)

Pane agent status feeds the pressure field: agent_status observations can deposit pheromones that boost scheduler wake on the herdr zone.

Quick start

castellan herd server          # background socket
castellan swarm-demo           # scheduler-led multi-pane demo
castellan herd attach          # interactive session

Attach modes

ModeCommandUse
Pollcastellan herd statusHeadless CI, scripts
Proxycastellan herd attachDaily-driver TTY

Plugin architecture

What Is a CastellanPlugin?

A plugin is a goal environment adapter: it encodes goals into runnable tasks, verifies observations against goals, and scores outcomes for evolution.

#![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![] }
}
}

Supporting traits:

  • GoalVerifier — verify-only surface for RLM loops
  • CrucibleAdapter — verify + automatic remediation hints (CruciblePluginAdapter)

Coordination hooks (optional, via context at runtime):

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

Current plugins

PluginKindLLMDescription
gridworldSimulatorNoDeterministic 5×5 grid; agents navigate to target; boids coordination deposits
shellExecutorNoBounded subprocess (argv, cwd, timeout); exit code + output verification
crucibleAdapterNoWraps any CastellanPlugin with remediation on failure

Lifecycle

register → encode → scheduler run → verify → episode snapshot → evolve fitness
  1. RegisterPluginRegistry::builtin() loads built-ins + manifest TOML
  2. Encodeencode_task(goal) produces initial TaskDescriptor payload
  3. Run — scheduler enqueues in plugin primary zone; host executes; observations flow back
  4. Verifyverify_goal returns AgentReport with NextAction
  5. Episode — runtime writes .castellan/episodes/<goal-id>.json
  6. Evolvecastellan evolve reads episodes; plugin name in environment_fingerprint seeds archive lookup

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. Discovery scans that tree at registry init.

Add a plugin in under 10 minutes

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

Installed plugins register automatically via plugins.toml + build.rs — no registry.rs edit.

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

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, HerdR paritySocket 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
pluginplugin
mcpmcp
telemetrytelemetry
doctordoctor
replayreplay
swarm-demo, rah-demodemos

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 10 --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. BL-020 (MOAT Phase 3): 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 Python GEPA 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

HerdR-shaped UX over the in-tree multiplexer — common attach, status, and tab operations without raw socket JSON.

Usage

castellan herd <SUBCOMMAND>

Subcommands include server, status, tabs, attach, wait, and related multiplexer helpers. Run castellan herd --help for the current list.

Recipes

Check multiplexer health:

castellan herd status

Attach to a running session:

castellan herd attach <session-id>

List tabs before attach:

castellan herd tabs

Daily-driver stack:

castellan multiplexer server &
castellan herd status
castellan dashboard

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 and harness tools from the shared registry.

Usage

castellan mcp

Methods: initialize, tools/list, tools/call. Same registry as castellan run --mcp live episodes.

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

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 socket, governance manifest, plugin registry, evolve archive, latest episode vitals.

Usage

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

Exits non-zero when any stage fails. --json for automation.

Stages

StageChecks
MultiplexerDefault socket reachable
GovernanceManifest / config load
PluginsRegistry matches plugins.toml
ArchiveTopology archive readable
VitalsLatest episode circulation metrics

Recipes

Local workspace check:

castellan doctor --workspace .

CI-friendly JSON:

castellan doctor --json | jq .

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 swarm coordination demo.

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

Per-subcommand pages with flags tables and recipes.

Start here: CLI overview

CommandPage
runrun
evolveevolve
herdherd
remoteremote
pluginplugin
mcpmcp
doctordoctor
replayreplay
Demosdemos

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)

See Memory architecture.

[governance]

KeyTypeDescription
session_spend_cap_usdfloatSession spend ceiling
max_risk_scorefloatRisk score threshold
denied_pathsstring[]Paths blocked for tool/file access

[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.

[models]

KeyTypeDescription
defaultstringFallback model spec for all roles (default: mock)
verifystringModel for the verify client
actstringModel for the act client
planstringModel for the plan client

Per-role tiers; the scheduler never routes through an LLM. --model-tier on castellan run selects a role at the CLI.

[[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_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-runtime/src/mcp/registry.rs
Stdio server: castellan mcp
Live episodes: castellan run --mcpCastellanEngine::call_mcp_tool

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:#8250df,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 HerdR-compatible automation methods; castellan-herdr is the NDJSON client.

Source: crates/castellan-multiplexer/src/api.rs · HerdR-compatible NDJSON automation (external herdr binary not 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.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
OLLAMA_URLunsetOptional local LLM for ignored integration tests

Session / remote

VariablePurpose
SSH_AUTH_SOCKUsed by castellan remote --ssh for agent forwarding

Hooks

See agent hooks for CASTELLAN_SOCKET in shell hook scripts.

Agent guide

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 control tower (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.

Full catalog: MCP tools.

Socket automation (HerdR-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

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 pane farm
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

Use Castellan in Cursor via MCP (castellan mcp). ACP is available for experiments but MCP is the primary editor 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. Restart Cursor.

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

MCP vs ACP

PathCursor use
castellan mcpPrimary — tool discovery, substrate read/write
castellan acpExperimental — in-process prompt bridge only

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.

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; 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

BL-020: 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.

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.

MCP overview

Castellan exposes stigmergy and harness tools via castellan mcp (stdio CLI) and live episodes (castellan run --mcp).

Start the server

castellan mcp

Wire your MCP client to stdin/stdout JSON-RPC (same surface as cargo run -p castellan-mcp).

Quick tool list

ToolDescription
castellan_discover_toolsBrowse tool domains
castellan_search_toolsKeyword search
castellan_describe_toolFull schema for one tool
castellan_deposit_signalDeposit stigmergic signal
castellan_read_blackboardRead blackboard slot
castellan_topology_snapshotHarness topology JSON
castellan_multiplexer_statusDashboard statusline
castellan_propose_mutationTopology mutation (governed)

Full catalog with live-column and governance notes: MCP tools.

Operational tools in LIVE_TOOL_NAMES are available on castellan run --mcp (see crates/castellan-runtime/src/mcp/registry.rs).

Live episode example

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

Governance pre/post hooks apply on the MCP hot path when --mcp is set.

Editor integration

Statusline and observability

Castellan exposes two complementary observability surfaces:

  1. statusline.json — dashboard pane/agent snapshot (MCP-readable)
  2. castellan run --json — typed castellan-events NDJSON stream

Both are produced by the same runtime; they serve different consumers.

statusline.json

Written by castellan dashboard to ~/.config/castellan/statusline.json (override with workspace config).

{
  "connected": true,
  "socket": "/Users/you/.config/castellan/castellan.sock",
  "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, "herdr_pulse": 0.4 },
  "live_pressure": { "zone_intensity": {} },
  "telemetry": { "agent_transitions": 42 }
}

MCP read path

Use castellan mcp tools or castellan dashboard --history for headless telemetry queries.

castellan-events schema (--json)

castellan run --json emits one serde-tagged event per line. Canonical variants:

EventWhen
episode_startBefore scheduler loop
scheduler_tickEach tick
wake_summaryAfter dispatch batch
task_dispatchedPer dispatched task
verify_resultPlugin verify
tool_decisionMCP / governance tool path
permission_deniedGovernance deny
budget_exhaustedDispatch cap hit
depositPheromone deposit
archive_write_backPost-run archive merge
episode_endAfter episode JSON written

Multiplexer events.subscribe can bridge the same schema via EmittedEvent::from_castellan_event.

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

./scripts/verify.sh
castellan drift-check

Crate boundaries

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

Docs

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

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

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
Workers subdomainhttps://castellan-docs.sgoldbeg.workers.dev (may vary by account)

Local build

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. mdbook build book → 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

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

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.

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 is not competing on

  • claw-code / OpenClaw — single-agent coding loops and provider matrices
  • Omnigent-style wrappers — composing external harnesses with policy YAML
  • LangGraph / workflow DSLs — hand-drawn agent graphs

What Castellan owns

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

Adoption framing

Use Castellan when coordination geometry must evolve from episode logs — not when you need a drop-in Claude Code replacement.

Stigmergy ablation

Method: In-tree honest proxies — not upstream Govcraft LLM meeting-scheduling (48.5% paper benchmark).

Summary

BenchStigmergyRandom / baselineNotes
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
Govcraft proxy (govcraft_acceptance)41.7% booking rate8.3% booking rateDiscrete slot scheduling with PheromoneField
Upstream GovcraftRequires API keys / Ollama; not bundled in-tree

Raw JSON (CI reproduce)

{"stigmergy_chain_completed":5,"random_chain_completed":0,"stigmergy_success_rate":1.0,"random_success_rate":1.0}
{"bench":"govcraft_acceptance","mode":"in_tree_honest_equivalent","stigmergy_bookings":200,"random_bookings":40,"stigmergy_rate":0.4166666666666667,"random_rate":0.08333333333333333}

Honest labeling

  • In-tree proxy: benches/govcraft_acceptance and benches/stigmergy_ablation use PheromoneField + scheduler semantics shaped after Govcraft pressure-field-experiment.
  • Not reproduced: LLM agent meeting scheduling at 48.5% vs 12.6% conversation baseline from the January 2026 paper.
  • CI: Both benches run in ./scripts/verify.sh on every PR.

Reproduce

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