CLI Reference

All commands accept -v / --verbose for informational messages on stderr. Legion is quiet by default. Data goes to stdout, errors to stderr.

This reference covers legion 0.40.0. Commands are organized by category.


Memory

reflect

Store a reflection from a completed session.

legion reflect --repo myproject --text "what you learned"
legion reflect --repo myproject --transcript /path/to/session.jsonl
legion reflect --repo myproject --text "..." --domain auth --tags "debugging,review"
legion reflect --repo myproject --text "..." --follows <parent-id>
legion reflect --repo myproject --whoami --text "identity reflection text"
legion reflect --repo myproject --text "..." --dedupe-mode strict

Prints the reflection ID (UUIDv7) to stdout. --repo accepts comma-separated names to store the same reflection across multiple repos.

Key flags:

  • --repo (required) repository name(s)
  • --text reflection content (mutually exclusive with --transcript)
  • --transcript path to a Claude Code transcript JSONL; legion extracts the last assistant message
  • --domain classification tag (auth, schema, color-tokens, etc.)
  • --whoami shortcut for --domain identity. Identity reflections are reinjected by the SessionStart hook on every boot
  • --tags comma-separated tags
  • --follows parent reflection ID; creates a learning chain
  • --dedupe-mode warn (default: store anyway), strict (refuse near-duplicates), off (skip check)
  • --force bypass near-duplicate detection regardless of --dedupe-mode

reflect retag

Move a live reflection between domains in place. Changes (or clears) domain without archiving, deleting, or re-issuing the id.

legion reflect retag --id <reflection-id> --set-domain workflow
legion reflect retag --id <reflection-id> --set-domain none

The reflection stays hot and recall-by-context still finds it; retagging only changes which domain banner it feeds (whatami reads domain=workflow, whoami reads domain=identity). Distinct from forget --persist, which moves a row to the cold tier — retag keeps it hot. Id, chain links, and recall_count are preserved.

Refuses to retag the last live root of a protected domain (identity or workflow) off that domain, since the insert-time zero-identity guard only fires on INSERT and retag must not become a side door around it. Use whoami --generate to replace an identity root deliberately.

Key flags:

  • --id (required) reflection to retag
  • --set-domain (required) new domain name, or the literal none to clear it

recall

Query reflections by relevance to a context string.

legion recall --repo myproject --context "vite cache problems"
legion recall --repo myproject --latest --limit 5
legion recall --repo myproject --domain identity
legion recall --repo myproject --context "..." --archives
legion recall --repo myproject --context "..." --include-archives
legion recall --repo myproject --domain workflow --since 7d
legion recall --repo myproject --context "cache invalidation" --on 2026-06-30
legion recall --repo myproject --context "vite cache problems" --tier card
legion recall --repo myproject --id <reflection-id>

Default mode blends BM25 full-text search with cosine similarity when embeddings are available (0.6 BM25 + 0.4 cosine). Results are further adjusted by boost/decay factors.

Key flags:

  • --repo (required) repository name
  • --context search query
  • --latest return most recent instead of most relevant; used by hooks when no meaningful query exists yet
  • --limit maximum results (default 5)
  • --domain return latest reflections matching this domain tag (bypasses search)
  • --cosine-only skip BM25, rank purely by cosine similarity (requires embeddings)
  • --min-score filter out results below this threshold
  • --preview truncate each result to N characters (used by hooks to keep injected context compact)
  • --tier card (id plus first sentence), gist (id plus lead paragraph), or full (default, today’s behavior). Progressive disclosure instead of every hit’s full stored text on every call. A tier only reports a truncation marker naming the completing --id when it actually cut something — a reflection whose card or gist IS its full text returns unmodified and names nothing. Mutually exclusive with --preview
  • --id fetch one reflection’s full body directly by id, bypassing search — the fault-in half of the tier contract, for a caller holding a card or gist that needs the rest. Fails with a named error (ReflectionNotFound, ReflectionRepoMismatch) rather than returning empty. --min-score does not apply on this path
  • --archives search only archived reflections (the deep dive; default searches hot only)
  • --include-archives search both hot and archived
  • --since / --until / --on filter by created_at, accepting YYYY-MM-DD, <N>d, <N>w, today, or yesterday. --on is sugar for --since X --until X. Applies as a hard predicate before ranking, in every recall mode including --domain and --latest

Date filtering is Tantivy-backed: an index built before legion 0.23.0 has no created_at field. Run legion reindex once after upgrading from an older version, or text-search-backed recall runs against an empty index until you do.

forget

Permanently delete a reflection by ID, or archive it instead.

legion forget --id <reflection-id>
legion forget --id <reflection-id> --repo myproject
legion forget --id <reflection-id> --persist

Default (no --persist) is destructive: no soft-delete, no undo, removes the row from SQLite and the Tantivy index. --repo is an optional safety check: the operation is refused unless the reflection’s repo matches.

--persist archives instead of deleting: the row moves to the cold tier, the search index entry survives, and it drops out of hot recall / whoami / whatami but stays reachable via recall --archives or --include-archives. One-way today — there is no un-persist verb yet.

boost

Mark a reflection as useful after recalling and applying it.

legion boost --id <reflection-id>

Increments recall_count, which multiplies the reflection’s future recall score by 1.0 + 0.1 * recall_count. A reflection boosted five times scores 1.5x its base.

similar

Find reflections similar to a given reflection by cosine similarity.

legion similar --id <reflection-id>
legion similar --id <reflection-id> --cross-repo --limit 10
legion similar --id <reflection-id> --json

Key flags:

  • --id (required) source reflection
  • --cross-repo include reflections from all repos (default: same repo as source)
  • --limit neighbors to return (default 5)
  • --min-score filter below this threshold
  • --preview truncate output text
  • --json machine-readable output

consult

Search across legion’s cross-agent surfaces. Two query modes, mutually exclusive.

legion consult --context "discriminated unions in composite rules" --limit 3
legion consult --context "cache invalidation" --since 7d
legion consult --context "cache invalidation" --tier gist
legion consult --symbol Database --json

--context searches reflections across every repo and returns results with repo attribution. --symbol searches SCIP indexes across every repo and reports (repo, lang, def_location, refs_count) per match. Used by the recall-first hook to inject cross-repo code intelligence before an agent spawns Explore.

--tier card|gist|full applies the same progressive-disclosure rendering as recall (default full); the fetch marker it prints is built from the hit’s own repo, not --cross-repo’s synthetic (all), so the printed recall --repo <r> --id ... command is always runnable as-is.

--since / --until / --on filter reflection mode by created_at (same grammar as recall); they have no effect on --symbol mode.

chain

Trace a learning chain from any reflection in it.

legion chain --id <reflection-id>
legion chain --id <reflection-id> --full

--full emits complete reflection text instead of the 80-character preview. Used by hooks that inject chain content as agent context.

whoami

Print identity reflections for a repo. Alias for recall --domain identity. A second mode, --generate, rebuilds an identity chain instead of printing one.

legion whoami --repo myproject
legion whoami --repo myproject --limit 50

# Gather mode: package source material for an identity rebuild
legion whoami --generate --repo myproject --vault-repo vault --byline "Sean Silvius"

# Apply mode: write the authored replacement from a manifest
legion whoami --generate --apply --from-file manifest.json --repo myproject
legion whoami --generate --apply --from-file manifest.json --repo myproject --dry-run

Gather mode packages two halves of source material as JSON: the claimed half (files in --vault-repo whose frontmatter author field matches --byline) and the given half (cross-agent reflections about this repo, over-fetched so self-repo rows can be filtered out). The binary authors no prose — the calling agent reads the JSON and writes an IdentityManifest.

Apply mode consumes that manifest and performs the swap: one transaction that retires every live identity root for the repo and inserts the new root (plus any chained children), with no two-live-roots or zero-root window. It backs up the full pre-existing identity corpus to a JSON file before touching anything, and explicitly retires any leftover old-chain rows the swap itself does not reach, in both the database and the search index. --dry-run reports the plan without writing or deleting anything.

Key flags:

  • --repo (required) repository name
  • --limit maximum identity reflections to return (plain listing mode; ignored with --generate)
  • --generate enter gather mode (default) or, with --apply, write an authored replacement
  • --vault-repo repo holding the agent’s bylined writing; required with --generate unless --apply is also set
  • --byline comma-separated author name(s) to match in frontmatter; required with --generate unless --apply is also set
  • --apply switch --generate into apply mode
  • --from-file path to the authored manifest (IdentityManifest JSON); required with --apply
  • --dry-run compute and report the apply plan without writing (apply mode only)

The identity-chain root itself is guarded below the CLI: inserting a second, unparented domain=identity reflection for a repo is refused unconditionally. whoami --generate --apply is the one sanctioned replace path for a repo’s identity root.

whatami

Print the operating contract for a repo. Alias for recall --domain workflow.

legion whatami --repo myproject

Distinct from whoami: whoami is who the agent is; whatami is how it operates on this repo.

surface

Surface cross-repo highlights for a session start.

legion surface --repo myproject
legion surface --repo myproject --since 7d

Gathers recent bullpen posts, high-value cross-repo reflections, and recently extended learning chains. Returns empty when there is nothing to surface.

--since / --until / --on apply across all three surfaced queries, with one exception: the recent-posts query defaults to a 24-hour window, and an explicit range overrides that default rather than composing with it.

reindex

Rebuild the Tantivy search index from the database.

legion reindex

Rebuilds reflections and documents together, in one commit, and reports both counts. Normally unnecessary during ordinary operation — but the search index’s on-disk schema changed to add document search, so the first daemon startup after upgrading from an older version wipes the existing index and starts empty; this is what repopulates it. Also use after manual database edits or corruption recovery.

backfill

Compute embeddings for all reflections missing them.

legion backfill

Requires the model2vec-rs model. Each node computes its own embeddings; they do not replicate across the cluster.


Team

post

Broadcast to the shared bullpen. No recipient, no wake. Use it for discoveries, decisions, FYIs.

legion post --repo myproject --text "OKLCH handles gamut mapping better than HSL for dark themes"
legion post --repo myproject --text "..." --domain color-tokens --tags "oklch,gamut"

Posts are stored as reflections with audience = 'team', making them discoverable via consult. Supports the same --domain, --tags, --follows, and --transcript flags as reflect. --repo accepts comma-separated names for multi-repo broadcast.

bullpen

Read the bullpen or check for unread posts.

legion bullpen --repo myproject
legion bullpen --repo myproject --count
legion bullpen --repo myproject --signals
legion bullpen --repo myproject --musings
legion bullpen --repo myproject --since 3d

Reading marks all posts as seen for your repo. --signals and --musings are mutually exclusive; without either, everything is shown.

Key flags:

  • --count print unread count only; silent when nothing is unread
  • --signals show only structured signals (posts starting with @)
  • --musings show only natural-language posts
  • --archived show archived posts instead of active ones; --archive archives posts every reader has seen
  • --include-stale / --include-resolved operator-review flags that surface past-TTL or resolved threads; agents should not pass these
  • --since / --until / --on filter by created_at (same grammar as recall); applies to the --repo listing, not to --count, --archive, or --archived

signal

Send a directed message to one agent.

# Wake-worthy: spawns an asleep recipient
legion signal --repo myproject --to backend --verb question --note "Should we cache auth responses?"
legion signal --repo myproject --to platform --verb handoff --status ready \
  --details "topic:auth,pr:42"
legion signal --repo myproject --to vault --verb rfc --details "budget:2h" \
  --note "Proposing new schema layout"

# Informational: delivered to live sessions only, does not wake
legion signal --repo myproject --to all --verb announce --note "Phase 2.1 shipped"

Wake-worthy verbs cause the watch daemon to spawn the recipient if asleep: question, request, handoff, correction, proposal, decision, routing. rfc is also wake-worthy but additionally requires a budget: entry in --details (e.g., --details "budget:2h").

Informational verbs deliver to a live session but do not wake an asleep recipient: announce, ack, info, answer. Delivery to a live session runs through inbox (see below), the hook-side lane. Silence is acknowledgment for informational signals; do not send empty acks.

answer itself stays off the wake-worthy list, but an answer that resolves the recipient’s own still-pending wake-worthy ask pages them anyway: legion signal checks, before composing, whether the recipient authored a pending wake-worthy ask this answer resolves, and stamps resolves:<ask-id> into the outgoing signal if so. A forged stamp never wakes — the referenced ask has to actually have been authored by the repo being woken, and be wake-worthy itself. So an agent that slept waiting for a reply does not stay asleep once the reply lands.

--status decorates the verb but does NOT affect wake routing. Only --verb gates whether watch spawns.

--note is limited to 280 characters. For larger content, use legion post and send a brief signal pointing to the post.

Key flags:

  • --to (required) recipient agent name, or all for broadcast
  • --verb (required) action verb (see above)
  • --status qualifier (approved, blocked, ready)
  • --note free-text note (max 280 chars)
  • --details comma-separated key:value pairs; required to include budget:<amount> for rfc
  • --follows parent reflection ID for threading

Replying to a signal retires the sender’s pending wake-worthy asks from your queue: legion signal --to X marks X’s REQUIRES A REPLY entries handled for this repo and reports how many it retired. Scoped to the agent being replied to — a broadcast address retires nothing — so answering one agent’s ask does not silently clear another’s. The retire happens after the send, so a failure there never costs the signal itself. This is host-local and distinct from resolve below: it clears this host’s inbox copy of the ask, not the thread itself.

resolve

Mark a bullpen post or signal thread as resolved.

legion resolve --id <post-id>
legion resolve --id <post-id> --reflection <converged-decision-id>

Resolved posts stop resurfacing in the bullpen and the watch wake-loop feed. --reflection links the converged decision so future recall surfaces them together.

resolve is the team-wide act: it hides the thread from every node’s bullpen. Replying to a signal (above) is not the same thing — it only clears your own pending-replies queue for that sender, on this host. A converged thread still needs resolve to stop resurfacing for everyone else.

pending-replies

Print a wake-prompt for pending wake-worthy signals.

legion pending-replies --repo myproject
legion pending-replies --repo myproject --directed

Called by the SessionStart hook to inject the “REQUIRES A REPLY” section when the agent wakes to directed questions or requests. --directed drops @all/@everyone broadcasts from the list, since a broadcast is not a personal ask; boot and post-compact keep the full set. The Stop hook runs --directed to decide whether a turn can end.

inbox

Deliver undelivered bullpen posts and signals into the current session. The sole live-session delivery lane — there is no separate push channel.

legion inbox --repo myproject
legion inbox --repo myproject --split

Also called by the plugin’s own hooks (UserPromptSubmit, PostToolUse, Stop), so it runs on its own even if you never type the command. It reads what is still undelivered for the repo and returns it as additional context at the next hook turn-boundary: on your next prompt, after any tool call, or at session end. That is real-time during an active turn; a live session sitting idle — no prompt, no tool call — used to hear nothing until one of those fired on its own. Watch now nudges an idle session in a watched repo that has unread mail, prompting it to take a turn so its own inbox delivers it; the nudge itself carries no post or signal content. Every delivery writes a DeliveryRecord row to delivery.jsonl.

--split sorts musings from directed signals in the binary instead of the hook, so the hook never parses posts. A directed signal’s entry in the inbox output is byte-identical to what legion pending-replies prints for it — one renderer, shared by boot, post-compact, and the inbox.


Work

GitHub issues are the work source, through legion’s own verbs. Direct gh is blocked for agents — everything below routes through the audit log instead.

issue

Manage issues via the configured work source plugin.

legion issue create --repo myproject --title "Implement auth" --body "..."
legion issue view --repo myproject --id 42
legion issue view --repo myproject --id 42 --json
legion issue list --repo myproject
legion issue list --repo myproject --state closed --label bug --json
legion issue close --repo myproject --number 42
legion issue close --repo myproject --number 42 --force --force-reason "hotfix, verify blocked on flaky CI"
legion issue reopen --repo myproject --id 42
legion issue edit --repo myproject --id 42 --title "new title"

legion issue view prints the raw issue: title, the stored body verbatim, state, and URL. Nothing is re-derived or reformatted — a body with N section headings prints all N, because the body is printed rather than parsed. --json is the machine surface: {number, title, state, url, preamble, sections}, built by splitting the body on ## headings without trimming. It carries a reassembly guarantee — preamble plus "## " + heading + content for every section, in document order, reconstructs the body byte-for-byte, including the EOF edge where a final heading with no trailing newline yields an empty content rather than inventing one. preamble is "" (never absent) when the body opens on a heading. Anything scraping issue content should read --json, never parse the plain-text output — the plain output is exactly the projection this release stopped trusting.

A body carrying both an ## Acceptance criteria and a ## Done When heading has both extend into the same criteria list, in document order, rather than the second overwriting the first; pr write-check and verify read the merged list.

legion issue list reads the work source’s live state directly through the same plugin path as issue view. --state filters open (default), closed, or all; --label filters by label.

An issue body can carry a ## Traces to section, pointing the issue at the requirement it implements:

## Traces to

- FR-2044-003 [criteria: crit-1, crit-3] -- users can filter results by date range
- None -- infra chore, nothing to trace

Each line either names a requirement id (FR-2044-003 above), with an optional bracketed list of criteria ids and a line of prose explaining the link, or is - None -- <reason>. legion issue create parses this section from --body and refuses to file the issue outright if the trace is malformed, names a requirement that does not exist, or points at a cancelled one — a bad trace is caught at filing time, not discovered later when verify runs against it. An issue with no ## Traces to section, or one that traces to None, remains a fully legal, untraced issue; tracing is opt-in, not a requirement placed on every issue. legion verify --issue and legion pr write-check --issue both read this section to decide whether they are judging the work against the issue’s own restatement of its criteria or against the traced requirement’s.

legion issue close reads the issue body through the same parser pr write-check --issue uses, and refuses to close unless a clean legion-verify:issue-<repo>#<n> verdict exists for every declared acceptance criterion. Absent and failed refuse with different messages: absent names the legion verify --issue command to run; not-clean says resolve the failing or uncertain criteria first. An issue with no declared acceptance criteria closes ungated, but it says so on stdout — an unchecked close that looks like a checked one is the exact failure the gate exists to close. The override is --force --force-reason "...", with the reason required, not optional; it records to the audit log’s OUTCOME field, not only the details JSON, because the default legion audit listing prints outcome and a bypass hidden in details sits where nobody looks.

The close path fails closed when it cannot read the issue at all: a work source that will not answer means the gate could not be evaluated, which is not the same as passing it. view-issue is a hard requirement of the close path — a plugin implementing only close needs it too.

work

Get the next candidate work item: the highest-priority open issue on the repo’s configured work source.

legion work --repo myproject

Sourced live from the work source’s issues (#931) — there is no local queue or board to fall out of sync with. --peek is present for CLI compatibility but is a no-op: there is no local “accepted” state left to claim against, so picking never claims anything.

comment

Post a comment on an issue or PR via the work source plugin.

legion comment --repo myproject --number 42 --body "..."
echo "..." | legion comment --repo myproject --number 42

--body is optional; omit it to read the comment from stdin, the only shell-safe way to pass a body containing backticks or $(...).

sub-issue

Link a child issue to a parent via GitHub’s native sub-issue relationship.

legion sub-issue create --repo myproject --parent 123 --title "..." --body "..."
legion sub-issue list --repo myproject --parent 123

push

The sanctioned in-band push path. Retires raw git push from agent doctrine.

legion push --repo myproject
legion push --repo myproject --branch feature/search
legion push --repo myproject --branch feature/search --force
legion push --repo myproject --branch feature/search --force --force-reason "base squash-merged, rebasing on main"
legion push --repo myproject --tag v0.39.0

Resolves the checkout that has the target branch checked out (via git worktree list) and pushes from that checkout, because the pre-push hook reviews the CWD’s checked-out branch, not the ref being pushed — pushing branch B from a checkout sitting on A would silently review the wrong diff. Refuses main/master and any --branch value shaped like a flag or a force/retarget refspec by construction. Sets upstream on every push (a no-op after the first) and audit-logs every attempt, success or hook-blocked failure, with the branch, resolved checkout, and head SHA.

A plain git push no longer needs to be remembered: a PreToolUse hook rewrites it to legion push and says so before running it. Anything the sanctioned command cannot express — a raw force push, a refspec, --delete, --tags, --mirror, --prune, --all — is denied outright, naming the flag, rather than silently dropped. A wrapper ahead of git push — env, sudo, timeout, nice, xargs, command, exec, time, or a bare VAR=val assignment — is denied outright too, never rewritten, for the same reason: the wrapper is information legion push has no way to carry.

--force is a gated force-push, not a raw passthrough. It fetches the remote ref, computes the commits the push would discard (reachable from the remote head but not the new local head), and checks each one for survival: present in the new history already (an ordinary amend or rebase), already on main by sha, or already on main by git patch-id equivalence — the squash-merge case, where a rebased stacked branch’s base just squash-merged into main. A squash that combined multiple commits into one main commit cannot patch-id-match any single pre-squash commit, so a cumulative fallback compares the combined content those commits touched against the new state directly before calling any of them an orphan. If every discarded commit survives, the push proceeds with no extra ceremony. If any commit is a genuine orphan, the push is refused, each orphan named as <sha> <subject>, and only --force-reason "..." overrides it — the same override contract as pr merge --merge-despite-failures and issue close --force. The push itself always runs with --force-with-lease=<branch>:<fetched-sha>, so a concurrent push from another agent or node loses the race instead of being silently clobbered. --force is mutually exclusive with --tag.

Key flags:

  • --repo (required) repository name, for the audit log
  • --branch branch to push; defaults to the CWD’s checked-out branch
  • --tag push a tag instead of a branch; mutually exclusive with --branch and --force
  • --force force-push, gated on the discarded-commit survival analysis above
  • --force-reason justification for overriding a force-push refusal; consulted (and required) only when the analysis finds a genuine orphan

commit

The sanctioned in-band commit path (#854). Closes the last unaudited hop in the mutation path — legion push routes pushes and the no-gh hook fences gh, but the commit itself, where authorship, trailers, and signing happen, used to be raw git.

legion commit --repo myproject --message "fix(auth): reject expired tokens before rate limiting"
legion commit --repo myproject --message-file commit-msg.txt
echo "..." | legion commit --repo myproject

Preflights the configured signer before touching anything, by signing a throwaway commit object — a locked or absent signer fails once, by name. Validates the message and refuses violations by name: the subject must be <type>(<scope>): <summary> with a scope, a body must be separated from the subject by a blank line, and no emoji anywhere. Since #1136, a subject-only message validates; a Co-Authored-By trailer is no longer required. Commits only the staged index — no -a, nothing is staged for you — and audit-logs every attempt, refusals included.

pr

Manage pull requests via the work source plugin.

legion pr create --repo myproject --title "..." --body "..." --closes 42
legion pr list --repo myproject
legion pr view --repo myproject --number 12
legion pr checks --repo myproject --number 12
legion pr checks --repo myproject --number 12 --log-failed
legion pr write-check --repo myproject --issue 42
legion pr edit --repo myproject --number 12 --title "corrected title"
legion pr edit --repo myproject --number 12 --body-file corrected.md --issue 42
legion pr comments --repo myproject --number 12
legion pr reviews --repo myproject --number 12
legion pr review --repo myproject --number 12
legion pr merge --repo myproject --number 12
legion pr close --repo myproject --number 12

legion pr create refuses unless both a clean legion-simplify gate and a clean legion-pr-write gate exist on the current HEAD commit. --closes <n> is repeatable and accepts a same-repo issue number or a cross-repo owner/repo#n reference (quote it — # starts a shell comment); it appends an idempotent Closes #N line to the body unless a recognized closing keyword for that exact issue is already present.

legion pr checks --log-failed streams the raw CI log for every failing job, each preceded by a ===== <name> (<job-id>) ===== header. Logs are fetched with gh api --allow-escape-sequences — required because cargo and other colored CI output trips gh’s default refusal to print terminal escape sequences — and ANSI color codes are stripped before printing, so the output is plain text. A genuine gh failure (expired retention, missing scope, wrong job id) is surfaced with gh’s own stderr in the error, not swallowed into a generic string.

legion pr write-check validates a drafted PR body against the issue’s acceptance criteria and records a legion-pr-write quality gate on success. Body from --body-file or stdin. When the issue traces to a requirement, it renders that requirement’s own criteria beside the PR body’s mapping and flags any criterion the body re-authors instead of citing verbatim — restating a spec’s wording, even accurately, is exactly where drift creeps in unnoticed.

legion pr edit corrects a live PR’s title and/or body in place — the honest alternative to close-and-recreate when a re-review finds a body that misdescribes its own diff. A --body-file edit runs the same structural validation pr write-check runs against --issue’s acceptance criteria and re-records the legion-pr-write gate for local HEAD; it is refused, not recorded, when local HEAD is not the PR’s own head commit.

legion pr merge refuses actively failing check-runs on the head SHA, not just absent ones, naming the failing checks. --merge-despite-failures is an audited operator override; it does not touch the separate zero-check-runs refusal, which still fires first. On a repo whose base branch merges through a GitHub merge queue, pr merge reports queued rather than merged — the queue completes the merge asynchronously, possibly after re-running CI, so issue-close side effects wait for the actual merge rather than firing early. Legion’s own repo merges through a queue with required checks; PRs land once the queue’s CI run is green, not the moment pr merge returns.

document

Store and retrieve coordination artifacts: specs, NFRs, blueprints, personas, journey maps, schemas.

legion document create --doc-type spec --owner vault --surface myproject --from spec.json
legion document create --doc-type schema --owner vault --from schema.json
legion document view <id>
legion document list
legion document list --doc-type requirement --surface myproject
legion document revise <id> --from revised.json
legion document validate --schema <schema-doc-id> --file instance.json
legion document archive <id>
legion document set-status <id> --to published

The storage layer is type-agnostic: payload is a validated JSON blob. Meta columns (type, surface, status, priority, owner) are indexed SQL columns.

set-status sets the lifecycle column, bumps updated_at, and returns the persisted row re-fetched from the database rather than constructed in memory, so a printed status is proof of the write. There is no status-machine enforcement; the operator setting the status directly — via this verb or the daemon’s POST /api/documents/{id}/status route — is the human gate, by design.

create and revise both validate the payload against the JSON Schema landed for its doc_type before writing. A schema document (doc_type=schema) still gets the structural gate first — $schema, title, type:object, non-empty properties, and a top-level x-doc-type naming the type it governs, since that keyword is what makes a schema self-describing and portable as a file. Every other type is checked against whichever schema declares that x-doc-type; a doc_type with no landed schema, or with two schemas claiming it, is refused outright — there is no warn-and-allow. A violation prints one <json pointer>: <message> line per problem to stderr and exits 1. A pointer reflection is stored automatically on every schema create or revise, on domain=schema, so legion recall --domain schema finds the current landed schema.

legion document validate --schema <id> --file <instance> runs the same check by hand against an arbitrary instance: the stored schema compiles through the jsonschema crate (draft-07 and 2020-12, $ref and definitions honored), and the instance is checked against it. One <pointer>: <message> line per violation, exit non-zero on any failure.

For a requirement document, legion document view also reports which of its verification.criteria currently carry a clean verify verdict, so a spec author can see coverage without cross-referencing every issue that claims to trace to it.

spec-gen (removed)

legion spec-gen is not a command in 0.40.0. It was removed in 0.34.0 (#821): deriving a requirement from a service-design document’s moment_of_truth is judgment work, not a deterministic field-concatenation, so it was pulled from the binary rather than kept as compiled Rust. The requirement JSON schema in the registry is unchanged. The current path for deriving a scope’s requirement set is the /legion:sd-write-spec skill, not a CLI verb — see the Plugin Guide’s skills section.

verify

Verify an issue’s acceptance criteria before it can close.

echo '[{"criterion":"search returns results","verdict":"pass","evidence":"test_search passes"}]' \
  | legion verify --repo myproject --issue 42

legion verify --repo myproject --issue 42 --verdicts-file /path/to/verdicts.json

Reads per-criterion verdicts as JSON: an array of {criterion, verdict, evidence} objects where verdict is pass, fail, or uncertain.

Decisions:

  • Every criterion pass with non-empty evidence: records a clean gate, allows the issue to close
  • Any fail: hard-blocks the close
  • Any uncertain or pass with empty evidence: routes to a human — the close is refused until a person resolves it
  • Issue declares no acceptance criteria: closes ungated, but says so on stdout rather than looking like a checked close

Criteria resolve one of two ways. An issue with no ## Traces to section, or one that traces to None, is verified straight from its own body via the same parser pr write-check --issue uses. An issue that traces to a requirement is judged against that requirement’s own verification.criteria instead — the spec’s wording, not the issue’s restatement of it — and the verdict pins the requirement document’s id and revision, so a criterion cannot quietly drift out from under a verdict that already passed it. legion issue create is the gate on the trace itself, refusing to file an issue whose trace names a requirement that does not exist, is cancelled, or is malformed. The verdict records as legion-verify:issue-<repo>#<n> — scoped by repo, since issue numbers are only unique within a work source. legion issue close reads this row.

quality-gate

Record, validate, list, and retire quality gate results for skill runs on a commit.

legion quality-gate record --skill legion-review --result clean
legion quality-gate check --skill legion-simplify --result clean --articulation-file notes.md
legion quality-gate check --skill legion-simplify --result issues --findings-json findings.json
legion quality-gate list --skill legion-simplify --branch feature/search
legion quality-gate stats --skill legion-simplify
legion quality-gate void --id <gate-id> --reason "manufactured clean, pre-#780 row"

legion pr create checks for clean legion-simplify and legion-pr-write gates on HEAD before opening a PR.

Every row carries provenance: VALIDATED (earned through quality-gate check’s articulation validator) or ASSERTED (self-reported via record). record --result clean is refused outright for a skill with a check validator (legion-simplify, legion-pr-write) — a clean verdict for those two can only be earned through check. Skills with no validator (legion-review, issue-keyed legion-verify:issue-<repo>#<n> rows) are asserted by necessity and unaffected.

legion quality-gate check parses an articulation file — markdown with one ### <path> heading per changed file, followed by substantive prose — and refuses to record a gate when a changed file has no entry, or an entry is boilerplate-thin. It resolves the changed-file set from <base>...HEAD; --base <ref> overrides the default (main, falling back to origin/main) for a branch stacked on another unmerged branch, so the coverage set is scoped to what this branch actually changed rather than everything since main. The resolved base — flag or default — is recorded on the gate row either way. Pure (zero-delta) renames auto-clear from the coverage set. --findings-json feeds structured {file, line, severity, summary} findings into the finding-resolution ledger; prose in the articulation is not parsed for findings, so a skill reporting --result issues should pass its real findings here.

Findings ledger: findings passed via --findings-json (or legion-review’s --details-json) persist as rows keyed to the gate that raised them. Resolution is git-log-based: a commit after a finding’s origin that touches the flagged file marks it RESOLVED, reconciled at the top of every record/check call. The gate refuses a clean verdict for a (branch, skill) while any HIGH/MED finding is pending or any LOW finding is un-acked.

legion quality-gate finding-list --branch feature/search --skill legion-simplify
legion quality-gate finding-disposition --id <finding-id> --reason "false positive, see PR #812 discussion"
legion quality-gate finding-ack --branch feature/search --skill legion-simplify --reason "cosmetic naming nits, batch-waived"

finding-list prints the full 36-character finding id — legion ids are UUIDv7, and two findings recorded close together can share most of a truncated prefix, so a short id is often ambiguous. finding-disposition --id and quality-gate void --id accept either the full id or an unambiguous prefix; more than one match is refused with every candidate named, never a silent pick.

finding-disposition retires one PENDING finding with a required reason — a disposition is not a fix, it is a recorded “not fixing this, and here is why.” finding-ack batch-acknowledges every PENDING LOW-severity finding on a (branch, skill) pair with one shared reason, still writing one row per finding so the audit trail stays per-finding. finding-list is the audit view: filterable by branch, skill, and status (pending, resolved, dispositioned).

legion quality-gate void --id <id> --reason <why> retires a known-false gate row without deleting it: a voided row drops out of the live gate lookups (pr create’s check, the ->Done gate) and out of stats, but stays visible in list with a VOID marker. --superseded-by <id> links it to the genuine replacement row once one exists.

audit

View the audit log of work source actions.

legion audit --repo myproject
legion audit --repo myproject --limit 20

Code intelligence

index

Build or refresh the SCIP code-intelligence index for a repo.

legion index myproject
legion index --file /path/to/changed.rs
legion index --status
legion index --status --json
legion index myproject --status --banner
legion index --logs
legion index --logs --repo myproject --follow

Detects languages by marker files: Cargo.toml (rust), package.json (typescript), pyproject.toml / requirements.txt (python), go.mod (go). Background-indexer logs land at ~/.local/state/legion/index-logs/ and survive reboots. Alongside the SCIP pass, legion index also walks the full file tree into file_inventory, parses JS/TS import edges, and extracts CSS symbols; those three passes run unconditionally, independent of whether a given language’s SCIP indexer binary is on PATH.

legion index <repo> --status --banner prints a SessionStart-friendly line per detected language. Silent when everything is fresh. A missing language now names why: “not indexed yet” means running legion index fixes it; “indexer unavailable” means the SCIP binary that language needs is not on this machine and this command cannot fix that by itself, though sym etc still answers non-symbol questions for that language’s files either way.

sym

Query SCIP symbol indexes in-process. No file scan, no language server runtime.

legion sym def Database
legion sym refs find_pending
legion sym impl Iterator
legion sym hover my_function
legion sym impact --repo myproject --diff /path/to/changes.diff
legion sym list --kind fn
legion sym list --lang css --repo myproject --kind custom-property
legion sym def --lang css --repo myproject -- --spacing-0.5
legion sym imports src/etc.rs --repo myproject
legion sym importers src/db.rs --repo myproject --json

sym impact parses a unified diff and reports SCIP reference counts per touched symbol, sorted descending. Pass --json for agent consumption. LEGION_IMPACT_HIGH_THRESHOLD (default 50) marks the HIGH tag in text output.

sym list replaces grep "fn " on indexed repos: returns names and locations, not source bodies. --lang css reads the separate lightningcss-extracted store instead of SCIP; --kind for CSS takes class or custom-property. sym def --lang css looks up a class or custom-property definition the same way. sym hover --lang css fails outright naming the commands that do work — CSS symbols have no hover surface.

sym imports <file> lists what a js/ts/jsx/tsx file imports, one row per static or dynamic import, resolved (to = Some) or unresolved/external (to = None); direct edges only, no transitive or re-export resolution. sym importers <file> lists every edge that resolved to a given file — the “who consumes X” question SCIP refs cannot answer. Both take a --repo filter (default: every indexed repo) and accept an exact path or a path suffix.

sym answers symbol questions. For everything else, sym tree and sym etc answer from the file inventory instead, and need no SCIP index at all.

sym tree

Structured, cross-repo view of the file inventory legion index builds. The sanctioned replacement for find / ls -R / a throwaway os.walk.

legion sym tree --repo myproject
legion sym tree --repo myproject --ext rs
legion sym tree --repo myproject --under src/db --depth 1
legion sym tree --json

Reads Database::list_file_inventory directly; never walks the filesystem at query time, so it answers instantly regardless of repo size. --repo and --ext filter server-side. --under scopes to a subtree prefix; --depth caps path segments counted from --under (or the repo root when --under is omitted). Omit --repo to get every watched repo’s files, each entry tagged with its own repo field.

--json (and sym imports/importers --json, and sym etc find-file --json) wraps results in a {snapshots, entries} envelope rather than a bare array. snapshots carries one row per repo in scope: indexed_at, head_at_index, current_head, and head_drift — computed with one live rev-parse per repo, never a filesystem walk. Human output prints the same freshness read as a stderr line per repo: up to date, a re-index hint when no snapshot exists, or a loud HEAD-drift warning naming both SHAs when the repo has moved since the last index.

sym etc

Non-symbol answer surface: query shapes over the files SCIP does not parse (docs, configs, css, prose). Three subcommands, one per query shape.

# Exact content search -- the sanctioned grep
legion sym etc find-content 'TODO' --repo myproject
legion sym etc find-content '--spacing-0\.5' --repo myproject --fixed-strings
legion sym etc find-content 'fn main' --ext rs --json
legion sym etc find-content 'API_KEY' --repo myproject --no-ignore --hidden
legion sym etc find-content 'TODO' --repo myproject --text

# One field out of a config file or doc frontmatter, without reading the whole file
legion sym etc extract package.json --field scripts.build
legion sym etc extract pages/docs/concepts.mdx --field title

# Locate a file by name, glob, or role, across every watched repo
legion sym etc find-file "*.test.ts"
legion sym etc find-file components.json --role config

find-content runs the same ripgrep engine as the shell tool it replaces, directly against the working tree: literal mode (--fixed-strings) matches conflict markers and punctuation-heavy patterns verbatim, regex is the default. --hidden reaches hidden files and dotfiles (.git/ stays excluded regardless). --no-ignore disables gitignore/.ignore/parent-ignore/git-exclude checks; it is independent of --hidden, so reaching a gitignored dot-directory (a generated .rafters/ workspace, for instance) needs both flags together. Gitignored files commonly hold secrets, and anything matched under --no-ignore is printed to stdout/JSON — treat it with the same care as --hidden.

As of 0.39.2, default output is locations only — path:line (or repo/path:line when the scan spans repos) — not the matched text, so a caller is not billed context for source it usually does not read. Pass --text to restore the old path:line: text shape; --json always includes the text regardless of --text. Default output also ends with a count line: N matches printed, or N matches printed (truncated at cap 500; M more suppressed) when the scan hits the cap. A successful zero-hit scan prints 0 matches printed rather than nothing — empty output is no longer the zero-hit signal.

extract reads JSON, TOML, YAML, or the YAML frontmatter of a .md/.mdx/.astro file and walks a dotted --field path; numeric segments index arrays. find-file matches basenames or glob patterns against the file inventory, cross-repo by default, with an optional --role filter (config, test, doc, entry). Every invocation of all three, success or failure, lands a row in etc-usage.jsonl.

sym def, refs, impl, list, and hover each print a per-repo freshness line to stderr — up to date, a re-index hint, or a HEAD-drift warning naming both SHAs — the same read sym tree gives on stdout. --json on these five stays a bare array/object, so the freshness read never reaches a --json consumer capturing stdout alone.


Runtime

watch

Watch for signals and auto-wake sleeping agents.

legion watch                          # start the daemon
legion watch add /path/to/repo
legion watch add /path/to/repo --name api --agent backend
legion watch remove api
legion watch list
legion watch leases
legion watch status
legion watch status --json
legion watch session-start --repo myproject
legion watch session-end --attempt-id <id>

The daemon samples health every 5 seconds and checks for wake-worthy signals every 30 seconds. Spawn is gated by pressure, cooldown (default 5 minutes), stagger (default 15 seconds between spawns), and a wake cap (default 4 in-flight).

watch status --json prints exactly one line: {"status":"alive|stale|absent","last_beat_age":"<text>"|null}. Clock skew between the daemon and the caller reads as stale. The prose form of watch status is unchanged; the boot banner’s watch line is rendered from this JSON form, so the two cannot drift apart silently.

defer / undefer

Defer a work item until a future time (#934) — legion’s own wake-me-up-at-time-T state, keyed on an opaque work-item id rather than a card row, since no work source has a field for this.

legion defer --work-item 42 --repo myproject --until 3d
legion defer --work-item 42 --repo myproject --until 2026-10-01 --note "waiting on upstream fix"
legion undefer --work-item 42

--until accepts YYYY-MM-DD, <N>d, <N>w, or today, parsed forward from now; it must resolve to a future time. undefer is a no-op, not an error, if the work item was not deferred.

daemon

Start the legion daemon (channel server + watch).

legion daemon
legion daemon --port 3131
legion daemon-spawn
legion daemon-stop
legion daemon-restart

autonomy

Weekly autonomy budget: governor on self-directed work.

legion autonomy status
legion autonomy gate --repo myproject --kind self-accept
legion autonomy gate --repo myproject --kind free-time
legion autonomy gate --repo myproject --kind free-time --operator

gate exits 0 and records spend when allowed; exits non-zero (cleanly) when the budget is exhausted. --operator bypasses the gate and records no spend.

mesh

Mesh-aware task placement.

legion mesh headroom
legion mesh pick

Ranks hosts by remaining rate-limit headroom and burn rate. pick prints the best hostname for a new task; exits 1 if no host is fresh.

statusline

Claude Code statusLine subcommand.

legion statusline
legion statusline --json

Wire via statusLine.command in settings.json. Persists rate-limit and usage samples each tick, then prints a one-line chip to stdout.


Misc

usage

Session token usage and cost analysis.

legion usage
legion usage --today
legion usage --since 2026-06-01
legion usage --by-session
legion usage --by-repo
legion usage --json

telemetry

Bypass telemetry: log and inspect when agents escape the grep/Read enforcement hooks.

legion telemetry record-bypass --tool Grep --repo myproject --pattern "fn init"
legion telemetry list-bypasses --repo myproject --since 24h
legion telemetry summary
legion telemetry etc-summary
legion telemetry etc-summary --command find-content --since 7d

summary surfaces the top under-served (tool, repo, pattern) tuples from bypass.jsonl.

etc-summary reads the sibling etc-usage.jsonl log and reports count, zero-result rate, and error count per sym etc / sym tree query shape (find-content, tree, extract, find-file). summary above is the bypass count; etc-summary is the adoption count, the number that answers whether the sanctioned replacement for grep/find actually gets used and actually answers.

uncertainty

Emit and witness predictions; read calibration metrics.

legion uncertainty emit \
  --surface legion.task \
  --feature-key scip.high-connectivity-refactor \
  --input-fingerprint <hash> \
  --claimed-confidence 0.85 \
  --payload '{"predicted_tokens": 5000000}'

# Resolve the model from the session instead of naming it
legion uncertainty emit --session-id <session-id> ...

legion uncertainty witness <prediction-id> --outcome-label shipped --outcome-correctness 1.0
legion uncertainty witness-gate --skill legion-simplify --commit <sha> --correct true

legion uncertainty calibration --surface legion.task
legion uncertainty orphans --json

legion uncertainty roll             # sweep orphans, then roll calibration -- the backfill entry point
legion uncertainty sweep-now --json
legion uncertainty calibrate-now --json

emit requires five flags: --surface, --feature-key, --input-fingerprint, --claimed-confidence, and --payload. --orphan-ttl-days defaults to 30, and 0 disables the orphan sweep for that row.

witness takes the prediction id as a positional argument, not a flag, plus --outcome-label (shipped, scoped-down, escalated, or abandoned) and --outcome-correctness in [0.0, 1.0]. An optional --payload carries the actuals.

witness-gate is the decorrelated path for quality-gate predictions specifically. It looks the prediction up by the same (skill, commit) fingerprint legion quality-gate list prints, so you never handle an opaque id, and --correct takes an explicit true or false rather than being a bare flag, which is what makes a negative witness as cheap to record as a positive one. Read --correct as relative to whichever verdict the gate recorded, not as a bare “was the diff clean”: on a recorded clean verdict, true corroborates it and false marks a clean verdict that shipped a bug; on a recorded issues verdict, true means the catch was right and false means it was a false positive. It errors when no emitted legion.gate prediction matches, which covers never-recorded, already-witnessed, and orphaned alike.

--model and --model-version are optional. Without --model, resolution falls to --session-id: it looks up the newest model seen for that session in the statusline samples. Pass neither and the row gets an explicit UNKNOWN_MODEL marker instead of a guessed id — filterable and honest, where a plausible-but-wrong default would quietly pollute a real cohort. emit is non-blocking: failures log to stderr and exit 0. witness is idempotent-failure: re-witnessing is an error. calibration shows one row per reliability bucket. orphans counts predictions never witnessed, grouped by surface.

roll, sweep-now, and calibrate-now are manual entry points into jobs the daemon otherwise runs on its own, inside the watch poll loop: an hourly sweep marks past-deadline emitted predictions orphaned, and a nightly roll computes calibration in fixed-width 0.1-point Brier buckets per cohort. roll runs both in order and is the one to reach for a one-off backfill. Each job is gated on an in-process last-run timestamp and fails open, so a job error never stops the poll cycle; legion daemon status does not report them — they log to stderr like the sibling reapers.

health

System health and recent trend.

legion health
legion health --history 1h
legion health --history 24h
legion health --all-hosts
legion health --json

stats / status / needs / now

legion stats                        # reflection statistics (all repos)
legion stats --repo myproject       # single repo
legion status --repo myproject      # team needs and what changed recently
legion needs --repo myproject       # what the team needs help with
legion now                          # current time, weekday, sunphase

init

Configure Claude Code hooks for legion (standalone install path).

legion init
legion init --force

cluster

legion cluster init
legion cluster init --key <64-hex-chars>
legion cluster key
legion cluster enable
legion cluster disable
legion cluster status

schedule

legion schedule create --name "daily standup" --cron "09:00" \
  --command "Good morning." --repo myproject \
  --active-start "08:00" --active-end "18:00"

legion schedule create --name "health ping" --cron "*/30m" \
  --command "Health ping" --repo myproject

legion schedule list
legion schedule enable --id <id>
legion schedule disable --id <id>
legion schedule delete --id <id>
legion schedule update --id <id> --cron "10:00"

Cron formats: HH:MM daily at that UTC time. */Nm every N minutes. Active windows support overnight ranges (e.g., 23:00-07:00).

rename / cleanup

legion rename --from oldname --to newname
legion cleanup

Environment variables

VariableDescription
LEGION_DATA_DIROverride the default data directory
LEGION_AUTO_WAKESet to 1 by the watch daemon when spawning agents
LEGION_SPAWN_SOURCESet to watch-pty on PTY-spawned wakes; the Stop hook early-exits when set
LEGION_SKIP_STOP_BLOCKSet to 1 to bypass the Stop hook’s block on an open directed ask; the bypass is audited
LEGION_REPOOverride repo name detection; takes precedence over basename($CWD) in every hook
LEGION_PORTOverride the channel server port (default: 3131)
LEGION_BYPASS_GREPSet to 1 to bypass the grep enforcement hook
LEGION_BYPASS_READSet to 1 to bypass the Read enforcement hook
LEGION_IMPACT_HIGH_THRESHOLDRef-count threshold for the HIGH tag in sym impact text output (default: 50)
WATCH_SPAWN_MODEpty (default) or print for the watch daemon spawn path