Architecture
Legion is a single Rust binary that stores data in SQLite, indexes prose with Tantivy, indexes code with SCIP, and computes embeddings with model2vec. No runtime dependencies. No background services required (watch, serve, and the daemon are optional).
Storage
SQLite via rusqlite with WAL mode. Default location: ~/.local/share/legion/ (XDG on Linux, Application Support on macOS). Overridable via LEGION_DATA_DIR.
Tables (grouped by concern):
Memory and search
reflectionsagent memories with BM25-searchable text, domain tags, learning chains, boost/decay scoring, archive/tombstone/LWW columnsembeddingsmodel2vec vectors stored as nullable BLOB on reflections (per-node, not replicated)
Code intelligence
scip_indexesper(repo, lang)SCIP protobuf blobs thatlegion symreads in-process. Built bylegion indexand refreshed by the PostToolUse hookfile_inventoryone row per non-ignored file in every watched repo, built by a gitignore-aware walk on everylegion indexrun. The substrate undersym treeandsym etc find-file; a file gets a row whether or not SCIP indexes its languagemodule_edgesone row per resolved (or unresolved) JS/TS import, export-from, or re-export, keyed on(repo, from_path, specifier). Built by parsing every js/ts/jsx/tsx file withoxc_parser/oxc_resolver, independent of whetherscip-typescriptis installed. Queried viasym imports/sym importerscss_symbolsone row per class-selector or--custom-propertydefinition, keyed on(repo, path, kind, name, line). Built withlightningcss, including Tailwind v4@themeblocks. Queried viasym list/sym def --lang css
Coordination
taskskanban cards with a 10-state machine, priority, labels, source URLs, timestamps, tombstone/LWW columns, pluswake_at/pre_defer_statusfor the Deferred stateschedulescron-like scheduled posts with tombstone/LWW columnsdocumentscoordination substrate documents (#455/#456): specs, NFRs, blueprints, personas, journeys, etc. Type-agnostic at the storage layer. Payload is a validated JSON blobboard_readsper-agent read cursors for the bullpen (local, does not replicate)
Watch and wake
watch_handledper-repo signal handling trackingwake_attempts(#487) FSM-tracked wake attempts: spawn time, PID, PTY EOF time, exit_observed_at, terminal status, and a nullablecard_idlinking an attempt to a Delegated card. The reaper is the only writer for terminal transitions
Observability
health_samplessystem telemetry for watch pressure monitoringrate_limit_samplesClaude Code rate-limit chips ingested bylegion statuslineusage_samplesClaude Code session token usage ingested alongside the rate-limit chipaudit_logwork source actions (issue/PR creation, merge, review) for traceabilityquality_gatesskill-run results enforced bylegion pr createand the ->Done gate, carrying provenance (VALIDATED via a check validator, ASSERTED viarecord), a void/supersede tombstone, and the resolvedbaseref acheckrun diffed againstquality_gate_findingsstructured findings (file, line, severity, status, disposition reason, resolving commit) keyed to the gate row that raised them, resolved by git-log detection or retired viafinding-disposition/finding-ack
Uncertainty (Pillar 2)
predictionsclaims with confidence, surface, cohort, orphan window (#355/#356)calibration_snapshotreliability buckets: claimed vs actual, count, Brier score (#357/#358)
All IDs are UUIDv7 (time-ordered). Migrations are idempotent and run on every DB open. Syncable tables (reflections, tasks, schedules, documents) carry deleted_at and updated_at for soft-delete replication and last-write-wins conflict resolution.
Identity-root guard (#785): a repo holds at most one live, unparented domain=identity reflection. insert_reflection_with_meta refuses a second one unconditionally — bootstrap and --follows chaining are unaffected, only a second orphan root is blocked. The one sanctioned replace path is swap_identity_root, consumed by legion whoami --generate --apply: a single transaction that deletes every live identity root for the repo, including any previously leaked duplicates, and inserts the new root plus optional chained children, rolling back atomically on failure. reflect retag carries the same guard from the UPDATE side: it refuses to retag the last live root of a protected domain (identity or workflow) off that domain.
Prose search
Tantivy BM25 full-text search with English stemming. Index lives alongside the database. Queries filter by repo or search across all repos (consult).
Incremental index sync detects reflections in SQLite missing from Tantivy and adds them automatically. No manual reindex needed after sync.
Embeddings
model2vec-rs for semantic similarity. Stored as a nullable BLOB column on reflections. Each node computes its own embeddings. They do not replicate.
Code intelligence
SCIP (Sourcegraph Code Intelligence Protocol) protobuf blobs in scip_indexes, keyed on (repo, lang). legion index detects languages from the workdir, runs the corresponding indexer per language (scip-typescript, rust-analyzer’s SCIP mode, scip-python, etc.), and writes the blobs.
legion sym def | refs | impl | hover | impact reads these blobs in-process. No file scan. No language server runtime.
Background indexer: triggered after legion watch add, after PostToolUse on indexed repos, and on demand. Logs land under ~/.local/state/legion/index-logs/.
Sym etc (#704): the non-symbol answer surface, for files and query shapes SCIP was never going to cover. sym etc find-content (src/etc.rs) runs an in-process ripgrep engine (grep-regex + grep-searcher over an ignore walk) against the working tree at query time, deliberately not a corpus: a tokenized index returns nothing on the punctuation-heavy literals agents actually search for, and any content index goes stale on checkout or pull, neither of which fires an edit hook. --no-ignore disables gitignore/.ignore/parent-ignore/git-exclude checks (independent of --hidden, which admits dotfiles — reaching a gitignored dot-directory needs both). sym tree, sym etc find-file, and the sym imports/sym importers module-graph readers answer from file_inventory/module_edges instead, with no filesystem walk at query time, and wrap --json output in a {snapshots, entries} freshness envelope (per-repo indexed_at, head_at_index, current_head, head_drift). sym list/sym def --lang css and sym etc extract round out the surface: CSS reads a separate lightningcss-backed store, and extract reads a JSON/TOML/YAML file or a .md/.mdx/.astro file’s YAML frontmatter and walks a dotted field path. Every invocation, success or failure, lands one row in etc-usage.jsonl; legion telemetry etc-summary reads it back.
Enforcement hooks (#438/#439, #713): on indexed repos, raw Grep and Read are blocked at PreToolUse. The redirect names the exact sym etc command for a non-symbol query shape, not just sym/recall: a literal string routes to find-content, a listing to tree, a config value to extract, “which repo has X” to find-file. Bypass via env var (LEGION_BYPASS_GREP=1, LEGION_BYPASS_READ=1) or # legion-bypass: <reason> sentinel in Bash. Every bypass writes one row to bypass.jsonl. This wording lives in the plugin (plugin/hooks/), not the daemon, so a session running an older plugin against a newer daemon can still show stale redirect text until the plugin itself is updated.
The feedback loop: bypass.jsonl is the signal that sym or recall missed an answer agents expected. The uncertainty engine reads it. The dashboard surfaces top under-served (tool, repo, pattern) tuples (#440). Doctrine becomes mechanism: the rule is enforced, the violations are counted, the count drives what gets fixed next.
Kanban scheduler
The kanban board is a scheduler wearing a human-friendly UI.
Card statuses: Backlog, Pending, Accepted, Needs Input, In Review, Blocked, Delegated, Deferred, Done, Cancelled.
State machine enforces valid transitions. force_move bypasses the state machine for dashboard drag-and-drop.
Delegated (#778) and Deferred (#816) are both fail-closed, not self-set labels. Delegated entry requires a fresh watch-daemon heartbeat and a live linked wake_attempts row; Deferred entry requires a resolvable future --until. Both revert through the same watch health tick that drives the rest of watch (reap_deferred_cards alongside the existing delegated reaper), writing status, wake_at, and pre_defer_status atomically so a crash between steps can never strand a card invisible to the sweep. Deferred is excluded from CardScope::WorkingSet — and so from the Stop gate’s accepted-only check — but stays visible everywhere else via list --deferred and its wake markers.
Scheduler (legion work): atomically selects the highest-priority unblocked card and accepts it. Priority ordering: critical > high > med > low, then sort_order, then oldest.
Work sources: plugins that sync external issue trackers (GitHub, GitLab, Jira) into the kanban board. Cards link back to source issues via source_url. Reconciliation pulls live state with legion kanban reconcile.
Sub-issues (#462): create child issues linked to a parent via GitHub’s native sub-issue relationship. The plugin looks up the parent node id, errors if missing, then links via the addSubIssue mutation.
Coordination substrate
documents is the type-agnostic storage layer for shared coordination artifacts: specs, NFRs, blueprints, personas, journey maps, schemas. Payload is a validated JSON blob; meta columns (type, surface, status, priority, owner) are hoisted into indexed SQL columns.
legion document create | view | list | validate | archive is the operator surface.
Schema registry: schemas are documents with doc_type=schema. They are structurally validated at insert (must have $schema, title, type:object, non-empty properties). Each dual-writes a pointer reflection on domain=schema so legion recall --domain schema surfaces every landed schema with its document id. legion document validate checks any JSON instance against a landed schema, one error per violation.
Spec-gen: legion spec-gen --repo <surface> reads non-archived service-design documents (types: persona, journey, blueprint, painmatrix, ecosystem), derives one requirement per moment_of_truth, validates each against the requirement schema, and inserts new requirement documents plus born-Backlog kanban cards. Idempotent on (traces_to, surface) pairs.
Card-spec binding: legion kanban bind --id <card-id> --document <doc-id> sets tasks.document_id. Once bound, legion verify reads acceptance criteria from the document’s verification.acceptance block. Status transitions that reach accepted, in-review, done, or cancelled also update the document’s meta.status transactionally (accepted, implemented, verified, cancelled). A dangling binding (document archived or missing) is a hard error on any transition.
Issue-requirement tracing (#933): an issue can point at a requirement without a card in between. Its body carries a ## Traces to section: a requirement id, an optional set of criteria ids, and prose, or an explicit None with a reason. legion issue create validates the trace at filing time, refusing a line that names a requirement that does not exist, is cancelled, or is malformed — a bad trace fails at the point it is written, not the point verify runs. legion verify --issue then judges the work against the traced requirement’s own verification.criteria rather than the issue’s restatement of them, and the recorded verdict pins the requirement document’s id and revision. legion document view surfaces which of a requirement’s criteria currently carry a clean verdict, and pr write-check renders the traced criteria beside the PR body’s mapping, flagging any the PR re-authors instead of citing. Untraced issues verify exactly as before, straight from the issue body.
Documents replicate across the cluster like reflections and cards.
Communication
Bullpen: shared message board. Posts are reflections with audience = 'team'. Discoverable via consult.
Signals: structured bullpen posts for coordination. Format: @recipient verb:status {details}. Filtered separately from natural-language posts. Wake-worthy verbs (question, request, help, blocker) spawn an asleep recipient via watch. Informational verbs (announce, ack, info, answer, review) deliver to live sessions only.
Resolve (#362): legion resolve --id <post-id> marks a converged thread so it stops resurfacing in the bullpen, channel notifications, and the wake-loop signal feed. Optional --reflection <id> links the team’s decision so future recall surfaces them together.
Channel: MCP server providing real-time communication tools. Events arrive as JSON-RPC notifications with method notifications/claude/channel. No polling.
Delivery (#941): two lanes carry a post or signal into a live session, running concurrently for parity measurement. The MCP channel pushes it as a JSON-RPC notification, no polling. legion deliver drain --repo <repo>, wired into the plugin’s UserPromptSubmit, PostToolUse, and Stop hooks, reads the same undelivered set and returns it as additional context at the next hook turn-boundary — a code-side path independent of the MCP notifier subprocess, with no inference roundtrip. Because both lanes run at once, a post can arrive via both. Both write a DeliveryRecord row to delivery.jsonl, so which lane actually delivered it is measured rather than assumed. The MCP channel is not removed.
Watch
Long-lived daemon that polls SQLite for unhandled wake-worthy signals. When one arrives for a configured repo, watch spawns a headless Claude Code session in that repo’s working directory.
Spawn modes (#485, env WATCH_SPAWN_MODE):
printlegacyclaude --printsubprocesspty(default since v0.16) portable-pty wrapper with a ring-buffered reader (#486, #498). Provides a real TTY so prompts that require interactive features behave correctly
Wake tracking (#487, #499): every wake attempt writes a wake_attempts row and transitions through an FSM. The reaper is the only writer for terminal states (exited, panic-stopped, abandoned). The Stop hook can hand off an exit_observed_at hint via legion watch session-end (#493). PTY EOF + PID poll remain authoritative.
Persona leases: cluster-wide wake coordination so two nodes do not wake the same persona at the same time. legion watch leases inspects.
Safeguards:
- Per-repo cooldown prevents wake storms
- Stagger between spawns prevents I/O storms
- System health monitoring pauses spawns under pressure
- Panic-stop on subscription-quota exhaustion (#484)
- Auto-unblock: when an agent announces completed work, blocked cards referencing that repo are automatically unblocked
Uncertainty engine (Pillar 2)
Agents emit predictions with claimed confidence. Outcomes are witnessed later. Calibration is measured per cohort and surface.
emit -> prediction row, orphan window starts
witness -> outcome recorded, calibration_snapshot updated
orphans -> predictions still unresolved past their window
calibration -> claimed vs actual per reliability bucket, Brier score
legion uncertainty emit is non-blocking so a downstream auto-emit hook (#358) can never break the agent. witness is idempotent-failure: re-witnessing is an error so the calibration table cannot drift.
Bypass telemetry (bypass.jsonl) feeds the same engine. A high bypass rate on a (repo, pattern) is a signal that the system claims it can answer the query (sym/recall) but agents are voting with their feet.
Rate-limit and mesh placement
legion statusline is wired into Claude Code’s statusLine.command. Each tick reads the rate-limit JSON on stdin, persists a rate_limit_samples row (and a usage_samples row when the transcript is readable), and prints a one-line chip.
legion mesh headroom ranks every host in the cluster by remaining rate-limit headroom and recent burn rate. legion mesh pick prints the best host for placing a new task. legion usage surfaces session cost analysis for the operator.
Multi-node
Each machine runs its own legion with its own SQLite database. Nodes on the same LAN discover each other via UDP broadcast and exchange encrypted delta packets. No coordinator, no central server, no cloud dependency.
Transport library: the broadcast and encryption primitives come from smugglr, a reusable Rust crate. Legion owns the schema, the cluster.toml configuration, the CLI, and the sync actor wiring on top. The split lets legion stay focused on the memory model while smugglr handles the edge-of-the-network concerns (packet framing, encryption, peer discovery).
Encryption: XChaCha20-Poly1305 with a pre-shared 256-bit key. The key is membership: anyone with the key can read and write to the cluster. cluster.toml is written with 0o600 on Unix.
Discovery: UDP broadcast on port 31337 (configurable). Each node announces itself and listens for peers.
Cross-network: bring your own tunnel (Tailscale, WireGuard, ZeroTier). Legion broadcasts to whatever subnet it’s on.
What replicates: reflections, kanban cards, schedules, documents. Each row carries updated_at (LWW) and deleted_at (tombstones), so deletes replicate as rows rather than vanishing from peers that have not yet seen them.
What stays local: Tantivy search index, model2vec embeddings, SCIP indexes, board_reads, health_samples, rate_limit_samples, usage_samples, watch_handled, wake_attempts, bypass.jsonl. Each node computes or tracks its own.
Tombstone housekeeper: legion watch runs a weekly job that hard-deletes rows whose deleted_at exceeds the retention window (7 days by default). Late-joining peers still see recent deletes. The database stays bounded.
cluster.toml
Stored in the data directory alongside legion.db. Managed by legion cluster * commands. Hand-editing is supported but unusual.
enabled = true
secret = "64-hex-chars" # 256-bit key, generated by `legion cluster init`
port = 31337 # UDP broadcast port
instance_id = "hostname" # optional, defaults to system hostname
Cluster commands
legion cluster init # generate key, write cluster.toml
legion cluster init --key <hex> # enroll using another node's key
legion cluster key # print the current key
legion cluster enable # start broadcasting
legion cluster disable # stop broadcasting (key preserved)
legion cluster status # peers, sync state, last sync time
Dashboard
Axum web server with rust-embed for static assets. SSE for live updates. Tabs: Feed, Signals, Board (kanban), Stats, Chat, plus code-intelligence views fed by scip_indexes and observability views fed by rate_limit_samples and usage_samples.
Document endpoints (#702): GET /api/documents, GET /api/documents/{id}, and POST /api/documents/{id}/status land in channel::router, the surface the daemon mounts, so the dashboard can list, view, and publish documents. The status route backs legion document set-status: the dashboard’s Publish/Approve button flips a document’s lifecycle status directly, with the localhost operator session as the human gate rather than a status-machine.
The dashboard is the human control surface. Agents interact via CLI.
Project layout
src/
main.rs -- CLI entry point (clap); the only process::exit site
cli/ -- subcommand handlers (kanban, pr, document, verify, spec_gen, watch, etc.)
db/ -- SQLite init, migrations, domain CRUD modules (reflections, kanban, documents, audit, etc.)
kanban/ -- CardStatus state machine and formatting
uncertainty/ -- emit, witness, calibration, orphans
mcp/ -- MCP stdio server (notifier, tools, log)
search.rs -- Tantivy index management
scip.rs -- SCIP index build, store, query (sym engine)
sym.rs -- sym subcommand: def, refs, impl, hover, impact, list, tree, etc
etc.rs -- sym etc: find-content (ripgrep engine), extract, find-file
inventory.rs -- file_inventory walk: the substrate under sym tree and find-file
graph.rs -- module_edges: JS/TS import graph via oxc_parser/oxc_resolver
css.rs -- css_symbols: class/custom-property extraction via lightningcss
recall.rs -- query and rank reflections
board.rs -- bullpen posts and filtering
signal.rs -- signal parsing, formatting, wake-worthy verbs
surface.rs -- cross-repo highlights
status.rs -- agent work status
documents.rs -- coordination substrate documents, schema registry, validation
spec_gen.rs -- requirement derivation from service-design artifacts
cluster.rs -- cluster.toml, key management, `legion cluster`
sync_actor.rs -- background thread that drives delta sync
serve.rs -- web dashboard
health.rs -- system health sampling
statusline.rs -- statusLine ingest + chip render
mesh.rs -- mesh-aware task placement
telemetry.rs -- bypass.jsonl and etc-usage.jsonl writers and readers
embed.rs -- model2vec embeddings
init.rs -- hook script generation and settings.json management
error.rs -- error types
plugin/
.claude-plugin/ -- plugin manifest
bin/legion -- wrapper script
hooks/ -- Claude Code hooks
commands/ -- slash commands
agents/ -- agent definitions (legion-prime, legion-explore, legion-verify, dungeon-master)
skills/ -- skill definitions (legion-memory, legion-simplify, legion-pr-write, legion-review, legion-verify)
channel/ -- MCP client bridge
worksources/ -- work source plugins