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.29.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)--textreflection content (mutually exclusive with--transcript)--transcriptpath to a Claude Code transcript JSONL; legion extracts the last assistant message--domainclassification tag (auth,schema,color-tokens, etc.)--whoamishortcut for--domain identity. Identity reflections are reinjected by the SessionStart hook on every boot--tagscomma-separated tags--followsparent reflection ID; creates a learning chain--dedupe-modewarn(default: store anyway),strict(refuse near-duplicates),off(skip check)--forcebypass 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 literalnoneto 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
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--contextsearch query--latestreturn most recent instead of most relevant; used by hooks when no meaningful query exists yet--limitmaximum results (default 5)--domainreturn latest reflections matching this domain tag (bypasses search)--cosine-onlyskip BM25, rank purely by cosine similarity (requires embeddings)--min-scorefilter out results below this threshold--previewtruncate each result to N characters (used by hooks to keep injected context compact)--archivessearch only archived reflections (the deep dive; default searches hot only)--include-archivessearch both hot and archived--since/--until/--onfilter bycreated_at, acceptingYYYY-MM-DD,<N>d,<N>w,today, oryesterday.--onis sugar for--since X --until X. Applies as a hard predicate before ranking, in every recall mode including--domainand--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-repoinclude reflections from all repos (default: same repo as source)--limitneighbors to return (default 5)--min-scorefilter below this threshold--previewtruncate output text--jsonmachine-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 --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.
--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--limitmaximum identity reflections to return (plain listing mode; ignored with--generate)--generateenter gather mode (default) or, with--apply, write an authored replacement--vault-reporepo holding the agent’s bylined writing; required with--generateunless--applyis also set--bylinecomma-separated author name(s) to match in frontmatter; required with--generateunless--applyis also set--applyswitch--generateinto apply mode--from-filepath to the authored manifest (IdentityManifestJSON); required with--apply--dry-runcompute 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, recently extended learning chains, and pending inbound kanban cards. Returns empty when there is nothing to surface.
--since / --until / --on apply across all four 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
Normally unnecessary. 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:
--countprint unread count only; silent when nothing is unread--signalsshow only structured signals (posts starting with@)--musingsshow only natural-language posts--archivedshow archived posts instead of active ones;--archivearchives posts every reader has seen--include-stale/--include-resolvedoperator-review flags that surface past-TTL or resolved threads; agents should not pass these--since/--until/--onfilter bycreated_at(same grammar asrecall); applies to the--repolisting, 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 two lanes at once, the MCP channel push and the hook-side deliver drain (see below) — either one can be the lane that actually lands it. Silence is acknowledgment for informational signals; do not send empty acks.
--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, orallfor broadcast--verb(required) action verb (see above)--statusqualifier (approved,blocked,ready)--notefree-text note (max 280 chars)--detailscomma-separatedkey:valuepairs; required to includebudget:<amount>forrfc--followsparent 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, channel notifications, 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
Called by the SessionStart hook to inject the “REQUIRES A REPLY” section when the agent wakes to directed questions or requests.
deliver
Drain undelivered bullpen posts and signals into the current session, outside the MCP channel.
legion deliver drain --repo myproject
Also called by the plugin’s own hooks (UserPromptSubmit, PostToolUse, Stop), so it runs on its own even if you never type the command. Where the MCP channel push notifies a live session in real time, deliver drain reads what is still undelivered and returns it as additional context at the next hook turn-boundary — a path that does not depend on the MCP notifier subprocess and costs no extra model call to arrive. Both lanes run at once, for now, so the same post can occasionally arrive twice. Every delivery on either one writes a DeliveryRecord row to delivery.jsonl, so which lane actually got a message in front of an agent is measured, not assumed.
Work
kanban
Manage the kanban board. Cards carry ten states with enforced transitions:
backlog -> pending -> accepted -> in-review -> done
| | |
| | +-> cancelled
| +-> delegated -> accepted (auto-revert or undelegate)
| +-> deferred -> accepted/pending (wake or undefer)
+-> blocked -> accepted (via unblock)
+-> needs-input -> accepted (via resume)
# Create
legion kanban create --from sean --to backend --text "implement search" --priority high
legion kanban create --from myproject --to backend --text "..." \
--source-url "https://github.com/owner/repo/issues/42" --source-type github
# View and list
legion kanban view --id <card-id>
legion kanban list --repo backend
legion kanban list --repo myproject --from
legion kanban list --repo myproject --deferred
# Transitions
legion kanban accept --id <card-id>
legion kanban block --id <card-id>
legion kanban unblock --id <card-id>
legion kanban review --id <card-id>
legion kanban need-input --id <card-id>
legion kanban resume --id <card-id>
legion kanban cancel --id <card-id>
legion kanban reopen --id <card-id>
# Delegated -- bound to a live watch-spawned wake attempt
legion kanban delegate --id <card-id>
legion kanban undelegate --id <card-id>
legion kanban delegated-needs-attention --repo myproject
# Deferred -- scheduled wake
legion kanban defer --id <card-id> --until 3d
legion kanban undefer --id <card-id>
# Spec revision -- stop rather than improvise around wrong acceptance criteria
legion kanban replan-request --id <card-id> --reason "AC assume a v1 API that shipped renamed"
legion kanban replan-record --id <card-id> --reason "AC 3 rewritten against the v2 endpoint"
# Bind a spec document
legion kanban bind --id <card-id> --document <doc-id>
# Other
legion kanban assign --id <card-id> --to backend
legion kanban update --id <card-id> --text "updated description"
legion kanban delete --id <card-id>
legion kanban reconcile --repo myproject
When a card has a document_id, status transitions that reach accepted, in-review, done, or cancelled also update the bound document’s meta.status in the same transaction. A dangling document_id (document archived or missing) is a hard error on transition.
Delegated: legion kanban delegate --id <card-id> hands an Accepted card to a live, watch-spawned wake attempt. Entry is refused unless the watch daemon’s heartbeat is fresh and a live wake attempt for the card’s repo actually exists — Delegated can never be a free self-set label with no process behind it. The watch health tick auto-reverts the card to Accepted the moment the attempt finishes or dies; legion kanban undelegate does the same manually. legion kanban delegated-needs-attention --repo myproject lists delegated cards whose linked attempt is NOT verifiably live — the fail-closed check the Stop gate runs for the one case the health tick cannot reach on its own: the watch daemon itself being down.
Deferred: legion kanban defer --id <card-id> --until <when> parks an Accepted or Pending card until a future time, excluding it from the Stop in-progress gate and from list’s default (working-set) view. --until accepts YYYY-MM-DD, <N>d, <N>w, or today, resolved forward from now; a value that has already passed is refused. The watch health tick reverts the card automatically once the wake time passes and pages its owner; legion kanban undefer --id <card-id> wakes it manually ahead of schedule. Deferred cards show up in list --deferred, carry a [wakes:YYYY-MM-DD] marker in list’s default output, and a Wake at: line in view.
Spec revision: acceptance criteria freeze when a card is accepted, and the failure mode is an agent deciding mid-build that they are wrong and quietly building something else. legion kanban replan-request --id <card-id> --reason <why> is the stop: it moves an Accepted card to NeedsInput and surfaces the reason, which is what a human re-ratifies against, so --reason is required rather than optional. Once the criteria are actually revised, legion kanban replan-record --id <card-id> --reason <what-changed> records that the revision was a deliberate design act. legion verify consults that record to tell a sanctioned re-plan from an unratified deviation, and fails the card on the latter. Neither command takes --repo; the card ID resolves it.
Both Delegated and Deferred share the same liveness caveat: the auto-revert only fires while legion watch is running for the card’s repo. If no watch process is alive when the wake condition is met, the card stays parked until one starts and runs a health tick — a late wake is a missed page, not a stuck agent, but it is worth knowing before you rely on it.
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 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 list reads the work source’s live state through the same plugin path as issue view — not the local kanban cache, which sync can miss entirely. --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 card_parse::parse_issue_body — 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, matching the card rule that lets a chore reach Done, 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.
Upgrade hazard: the gate reads the issue-keyed verdict only. Work that has a card, verified card-keyed as legion-verify:<card_id>, and then closed with legion issue close instead of legion done finds no issue-keyed row and is refused for work that already passed verify. If a card exists, close it with legion done — that path resolves the work source directly and is unaffected by this gate.
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
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; there is no --force flag. 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 force push in any spelling, a refspec, --delete, --tags, --mirror, --prune, --all — is denied outright, naming the flag, rather than silently dropped.
Key flags:
--repo(required) repository name, for the audit log--branchbranch to push; defaults to the CWD’s checked-out branch
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 the kanban-done transition and 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 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 clicking Publish in the dashboard is the human gate, by design.
Schema documents (doc_type=schema) are structurally validated at create: payload must include $schema, title, type:object, and non-empty properties. Other document types can be validated against a landed schema via legion document validate --schema <id>. A pointer reflection is stored automatically on domain=schema so legion recall --domain schema finds every landed schema.
legion document validate checks a JSON instance against a landed schema. One error per violation, exits 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
Generate requirement documents from service-design artifacts.
legion spec-gen --repo myproject
Reads all non-archived service-design documents whose surface field matches --repo (types: persona, journey, blueprint, painmatrix, ecosystem). Note: --repo here is the functional surface label stored on documents (e.g., payments, onboarding), not a git repository name. Derives one requirement per moment_of_truth. Inserts new requirement documents and born-Backlog kanban cards. Re-running on unchanged input is safe: existing (traces_to, surface) pairs are skipped.
verify
Verify a card’s acceptance criteria before it can reach Done.
echo '[{"criterion":"search returns results","verdict":"pass","evidence":"test_search passes"}]' \
| legion verify --repo myproject --card <card-id>
legion verify --repo myproject --card <card-id> --verdicts-file /path/to/verdicts.json
# No card involved -- criteria come from the issue body, or the traced requirement if one exists
echo '[{"criterion":"search returns results","verdict":"pass","evidence":"test_search passes"}]' \
| legion verify --repo myproject --issue 42
Reads per-criterion verdicts as JSON: an array of {criterion, verdict, evidence} objects where verdict is pass, fail, or uncertain.
Decisions:
- Every criterion
passwith non-empty evidence: records a clean gate, allows->done - Any
fail: hard-blocks the card - Any
uncertainorpasswith empty evidence: routes toneeds-inputfor a human - Card has no acceptance criteria: blocked outright
When a document_id is bound, criteria come from the document’s verification.acceptance block first; a missing or empty block falls back to tasks.acceptance. A dangling document_id (the document does not exist) is a hard error, not a silent fallback.
--card and --issue are mutually exclusive, and exactly one is required. The --issue path verifies work with no card at all, and 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. Either way the verdict records as legion-verify:issue-<repo>#<n> — scoped by repo, since issue numbers are only unique within a work source, and namespaced so it cannot collide with a card id. legion issue close reads this row. The issue path still does less than the card path in one respect: no status transition, since there is no card to move; and --deviation refuses outright, since that gate is adjudicated against a card’s ratified ReplanRecord and there is none to check against on this path.
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, a card-keyed legion-verify:<card_id>) 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.
done
Announce completed work and notify blocked agents.
legion done --repo myproject --text "search endpoint shipped"
legion done --repo myproject --id <card-id> --text "search endpoint shipped"
Marks the card complete and posts an announce signal. When --id is supplied and the card has acceptance criteria, a clean legion verify verdict must exist before Done is accepted; the command exits non-zero otherwise. The watch daemon auto-unblocks blocked cards whose block reason mentions the completing repo.
work
Get the next work item from the scheduler.
legion work --repo myproject
legion work --repo myproject --peek
Atomically selects the highest-priority unblocked card assigned to the repo and accepts it. Priority: critical > high > med > low, then sort_order, then oldest. --peek shows without accepting.
sync
Sync issues from the configured work source into the kanban board.
legion sync --repo myproject
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
# 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.
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.
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 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).
daemon
Start the legion daemon (channel server + watch).
legion daemon
legion daemon --port 3131
legion daemon-spawn
legion daemon-stop
legion daemon-restart
goal
Print the active Accepted card’s acceptance criteria framed as a completion condition.
legion goal --repo myproject
Called by the SessionStart hook each session. Empty when nothing is in progress.
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
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.
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 # work state and team needs
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
mcp-health / mcp-logs
legion mcp-health # probe a fresh MCP subprocess for notifier health
legion mcp-logs
legion mcp-logs --tail
mcp-health spawns a NEW MCP; it does not probe the MCP attached to a running Claude Code session.
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>
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
| Variable | Description |
|---|---|
LEGION_DATA_DIR | Override the default data directory |
LEGION_AUTO_WAKE | Set to 1 by the watch daemon when spawning agents |
LEGION_SPAWN_SOURCE | Set to watch-pty on PTY-spawned wakes; the Stop hook early-exits when set |
LEGION_REPO | Override repo name detection in the MCP channel |
LEGION_PORT | Override the channel server port (default: 3131) |
LEGION_BYPASS_GREP | Set to 1 to bypass the grep enforcement hook |
LEGION_BYPASS_READ | Set to 1 to bypass the Read enforcement hook |
LEGION_IMPACT_HIGH_THRESHOLD | Ref-count threshold for the HIGH tag in sym impact text output (default: 50) |
WATCH_SPAWN_MODE | pty (default) or print for the watch daemon spawn path |