Skip to main content

CLI

mecha [GLOBAL OPTIONS] <COMMAND> [ARGS]

mecha --help lists the commands; mecha <command> --help prints the full flag set for one. mecha --version prints the build.

Global options

These are declared once and accepted by every subcommand, including the ones that never build an agent (where they are simply ignored).

FlagDescription
-p, --provider <PROVIDER>Provider to use, by config key. Defaults to the config's default_provider.
-m, --model <MODEL>Model id, overriding the provider's default.
-e, --effort <EFFORT>Reasoning depth: low, medium, high, xhigh, max.
-s, --system <SYSTEM>System prompt. Use @path to read it from a file.
-w, --workspace <WORKSPACE>Directory the agent may read and write. Defaults to the working directory.
-y, --yesApprove every tool call without asking. Required for unattended runs that need to write or execute.
--read-onlyRefuse anything that is not read-only. Conflicts with --yes.
--max-turns <N>Stop after this many model turns.
--max-output-tokens <N>Stop once the run has generated this many output tokens.
--max-cost <USD>Stop once the run has cost this much. Needs prices configured on the provider.
--tool <NAME>Only expose these tools. Repeatable; names are matched exactly.
--tool-profile <PROFILE>Narrow to research, assistant, or coding; composes with --tool.
--no-mcpSkip MCP servers entirely.
--no-mcp-server <NAME>Skip these MCP servers by name. Repeatable; for turning one off while the rest stay.
--no-thinkingTurn off reasoning. Cheaper and faster, but noticeably worse on multi-step work.
--no-skillsDon't load skills from ~/.mecha/skills — no skill tool, and nothing about them in the system prompt.
--skill <NAME>Only carry these skills. Repeatable, and narrows what [skills] already selected — it cannot enable one config withheld.
--no-charterDon't load ~/.mecha/charter.toml into the system prompt — the run proceeds with no standing priorities.
--no-learned-rulesDo not inject learned rules from ~/.mecha/learning into the system prompt.
--no-hooksDo not run configured [[hook]] commands. Config is still validated.
--no-outboxDo not route any tools through the outbox; configured [outbox] tools execute directly.
--no-messagesNo inter-agent messaging: no message_send tool, and nothing from the mailbox is delivered into this run.
--no-fallbackNever fall back to another provider. A transient failure that survives its retries fails the run.
--no-compact-toolWithhold the model's compact tool; automatic compaction remains available.
--no-step-escalationDisable quarantined checks of ambiguous completed plan steps.
--no-boredomDisable notices about approaches that stop yielding new evidence.
--no-compact-validateSkip omission checks on compaction summaries.
--no-predictive-compactionTrigger on reported context size only; output budgeting and headroom forecasts remain active.
--no-carried-stateDo not carry tool-owned plan state across compaction.
--compact-at <N>Summarise older turns once the prompt passes this many tokens.
-v, --verbosePrint tool calls, results, and token usage as they happen.
-h, --helpPrint help.
-V, --versionPrint the version.

MECHA_LOG controls internal tracing, which goes to stderr and is independent of --verbose:

MECHA_LOG=debug mecha run "what changed today?"
MECHA_LOG=mecha_core::mcp=trace mecha tools # one module only

The default filter is warn.

run

Run one task and print the answer.

mecha run [OPTIONS] [PROMPT]
FlagDescription
[PROMPT]The task. Omit it, or pass -, to read from stdin.
--jsonEmit a single JSON object instead of prose. Implies --quiet.
--quietPrint only the answer — no tool narration.
--no-streamWait for the whole answer instead of streaming it.
--resume <ID>Continue a saved session by id or unique prefix.
--goal <KIND:ID>Explicitly confirm this run's goal.
--appraisal-evidence <FILE>Owner-authored commitment/check evidence for the matching --goal; see anticipatory appraisal.
--no-sessionDo not write a transcript.
--image <PATH>Attach image pixels to the user turn. Repeatable; requires a vision-enabled provider.

Exit codes: 0 success, 1 error, 2 the model refused, 3 it produced no answer at all. Exhaustion is deliberately not a failure code — a run stopped by a turn, token or cost ceiling that still answered exits 0, because the work it left behind is graded on its own terms. --json's stop_cause names the ceiling for callers that care which one it was.

Approval prompts are only offered when stdin is a terminal and --json was not passed; otherwise the configured permission_mode decides.

mecha run "summarize what changed in this repo today"

# Unattended, bounded, machine-readable.
mecha run --json --yes --max-cost 0.25 \
-w /srv/reports "regenerate the weekly summary in reports/weekly.md"

# Piped in, continuing an earlier session.
git log --oneline -20 | mecha run - --resume 20260805T091500

chat

Interactive session in the terminal, with history and slash commands.

mecha chat [OPTIONS]
FlagDescription
--resume <ID>Continue a saved session by id or unique prefix.
--no-sessionDo not write a transcript.

Slash commands: /tools, /model, /usage, /clear, /session, /help, /exit (also /quit, /q). /clear starts a new conversation, dropping its taint along with its messages. Ctrl-D exits.

mecha chat -p local -m qwen3-14b

tui

Full-screen session. The input line stays live while the agent works, so a message typed mid-run steers it instead of waiting for it.

mecha tui [OPTIONS]
FlagDescription
--resume <ID>Continue a saved session by id or unique prefix.
--no-sessionDo not write a transcript.

Drop image files onto the TUI input line to attach them when the provider supports vision; there is no launch-time image flag.

Slash commands:

CommandDescription
/helpThe list.
/toolsTools this agent can call.
/triggersScheduled prompts: see, edit, run, cancel.
/outboxStaged sends and publishes: show, edit, send, reject.
/queuesEvery store waiting on you — incl. the graph's merge queue, reviewed in place.
/frontdoorInbound requests: extract, triage, close.
/pollsOpen polls, their tallies, and the lecture controls.
/review [now|later|auto]What happens to drafts a run stages.
/model [id]Show or switch the model.
/provider [name]Show or switch the provider.
/mode [ask|allow|read-only]Show or switch the permission mode.
/mcp [on|off]List MCP servers, or turn them all off and on.
/mcp <server> [on|off]Turn one server off and on.
/usageTokens used this session.
/clearStart a new conversation, dropping its taint.
/sessionWhere the transcript is being written.
/todoShow or hide the live task pane.
/exitQuit.

The status line shows context use as a fraction of the window when [providers.X] context_window is configured. Steering is a property of this front-end: it needs one owner of stdin, which a readline REPL cannot be while a run is streaming.

mecha tui -w ~/code/my-project

serve

Serve the web app on loopback behind Tailscale. Configure [web] owner_login in the global config; assets points to a separately built copy of web/dist.

FlagDescription
--port <PORT>Override [web] port (default 63242).
--assets <PATH>Override the directory containing the built app.
--owner-login <LOGIN>Override the permitted Tailscale identity.
--voice-port <PORT>Mounted voice facade; default 8990, 0 disables.
--voice-yesAllow voice calls without per-call approval; configured outbound actions still stage.
--offer-target <URL>Voice worker offer endpoint; default http://127.0.0.1:7860/api/offer. Empty disables proxying.
mecha serve --assets ./web/dist

The browser app sends X-Mecha-Request: 1 on mutations. Scripted API clients must send it too and explicitly open a chat with POST /api/chat/{key} before reading its transcript or events. See The web surface.

voice-serve

Standalone loopback facade for the local voice worker. The normal browser setup mounts this inside mecha serve; use a separate process when that separation is needed. The transcription and speech stack is installed separately.

mecha voice-serve --port 8990

--port defaults to 8990; --token optionally requires a bearer token. See Voice for the worker and service setup.

batch

Run the same agent over a JSONL file of prompts, with bounded concurrency.

mecha batch [OPTIONS] <INPUT>
FlagDescription
<INPUT>JSONL input. Each line is {"id": "...", "prompt": "...", "meta": {...}}, or a bare string used as both id and prompt. - reads stdin.
-o, --out <OUT>Where to write results, one JSON object per line. Defaults to stdout.
-c, --concurrency <N>How many items to run at once. Default 4.
--limit <N>Stop after this many items. Useful for a smoke test over a big file.

Results stream to the output file as they finish, keyed by id, so a killed run still leaves everything completed so far on disk. Each item gets a fresh conversation, and therefore fresh taint.

# items.jsonl
# {"id": "q1", "prompt": "who did I meet with last week?", "meta": {"gold": "..."}}

mecha batch items.jsonl --concurrency 8 --out results.jsonl --yes
mecha batch items.jsonl --limit 3 -c 1 -v # smoke test first

eval

Score a model on a case set, grading the tool-call trace first and the text second.

mecha eval [OPTIONS] [CASES]
FlagDescription
[CASES]JSONL case file. Default eval/cases.jsonl.
--fixture <PATH>Workspace the agent reads during the run. Defaults to a workspace directory beside the case file.
-o, --out <OUT>Write the full scorecard and per-case detail here as JSON.
-c, --concurrency <N>How many cases to run at once. Default 4.
-k, --runs <K>Run every case K times and report pass^k beside pass@k. Default 1.
--tag <TAG>Run only cases carrying this tag. Repeatable.
--failuresShow every failed check, not just the count.
--judge-model <MODEL>Model that grades expect.judge rubrics. Defaults to the model under test.
--judge-provider <PROVIDER>Provider entry the judge model runs on. Defaults to the one under test.
--keep-workspacesKeep the staged workspaces of sandboxed cases instead of deleting them.
--mcpConnect MCP servers during the eval. Off by default for reproducibility.
--mcp-file <PATH>Connect exactly the servers named in this TOML file, instead of the machine's config.
--no-ask-userWithhold ask_user, which is otherwise part of the tool surface.
--ab-rulesRun the set twice — rules-free, then with this machine's learned rules — and report the per-case flips.
--ab-config <KEY=VALUE>Run the set twice, differing only in this override, and judge the difference against a holdout. Repeatable.
--holdout-in <N>One case in N is held out of selection, for --ab-config. Default 3.
--compare <FILES>...Compare previously written scorecards side by side instead of running.

mecha eval exits non-zero when anything fails, so it works as a regression gate. It forces MCP off, hooks off, learned rules off, the outbox off and fallback off, so a scorecard grades the model it names rather than this machine's local setup.

--runs k matters more than it looks: reliability decays much faster than mean success, and the gap between pass^k and pass@k is the model's unreliability. A pinned seed at --concurrency 1 replays token-for-token, making the k samples one sample counted k times; the harness warns when it detects that.

--mcp-file resolves relative paths in a server's command/args against the file's own directory, and a server that fails to connect is fatal here.

--ab-config overrides a closed set of run options — compact_at_tokens, max_turns, max_output_tokens, effort — so both arms are built by one code path. Unknown keys are refused, and every override is parsed before the first arm runs. Neither arm is filed as an ordinary scorecard, and it always exits 0: a delta is a finding, not a gate.

mecha eval -p local -m qwen3-moe -o results/qwen.json
mecha eval -p anthropic -o results/opus5.json
mecha eval --compare results/*.json

mecha eval --tag chaining --failures # one slice, with reasons
mecha eval -k 5 -o results/qwen-k5.json # pass^5 beside pass@5
mecha eval eval/graph-cases.jsonl --mcp-file eval/mcp.toml
mecha eval --ab-config max_turns=40 # measure a proposed change

exp

Run a designed comparison with named arms and isolated homes. The manifest fixes the tasks and treatments before trials run; this is separate from mecha eval.

mecha exp new eval/assistant-lifetime.toml
mecha exp run assistant-follow-through --dry-run
mecha exp run assistant-follow-through --limit 3
mecha exp status assistant-follow-through
mecha exp judge assistant-follow-through --json
mecha exp export assistant-follow-through

new refuses an existing name. run resumes unfinished trials and skips finished ones. See Experiments for manifests, fixtures, and how to interpret the gate.

tools

List the tools an agent would see. Runs without any provider configured, which makes it a good MCP-server smoke test.

mecha tools [OPTIONS]
FlagDescription
--schemaPrint the full JSON schema for each tool, exactly as the model sees it.
--jsonEmit JSON instead of a table.

The output names the active sandbox backend, and --json includes each tool's capabilities. Subagent profiles are shown with the tools they were granted, with a warning when a profile holds all three legs of the trifecta.

mecha tools
# Anything that can send at all:
mecha tools --json | jq '.[] | select(.capabilities.external_send)'
# Only the ones the trifecta interlock refuses — where the model picks the
# recipient. `blind` senders such as `web_search` are not among them.
mecha tools --json | jq '.[] | select(.capabilities.egress == "chosen")'
mecha tools --schema --no-mcp

skills

List the skills an agent would carry — the procedures you have written in ~/.mecha/skills/, and which of them this run would load. Builds no provider and connects to nothing.

mecha skills [--show] [--json]
FlagDescription
--showPrint each skill's full body, exactly as the model would receive it.
--jsonEmit JSON instead of a table.

A skill config withholds is listed with a - rather than omitted, so "why is this not firing" is answerable here instead of by intersecting two config files by hand. Exits non-zero when a SKILL.md failed to parse, so it works as a check in a script; a store that is merely empty is healthy.

mecha skills
mecha skills --show
mecha skills --json | jq '.skills[] | select(.carried)'

See Skills.

charter

Print the standing priorities in ~/.mecha/charter.toml, in rank order, as a run would see them.

mecha charter [edit] [--json]
SubcommandFlagDescription
(none)--jsonEmit JSON instead of a table — including on failure, so a scripted consumer sees the parse error in the payload rather than only in the exit code.
editOpen the charter in $EDITOR, creating a commented template first if there is no file yet.

The owner may edit it; a model never authors a line of it. That is the invariant, and it is worth stating precisely because this page used to state it as "there is no edit and never will be" — which was a misstatement rather than a decision: the TUI's /charter already handed the file to $EDITOR, and the web settings page already took a validated save, so the rule as written made the command line the only surface where you could not edit your own document.

So edit hands over $EDITOR on the file itself. mecha writes exactly one thing here ever: the comments-only template, when no file exists yet, because vi on an empty buffer is how a first charter ends up shaped wrong. There is still no --add, no --set and no tool — nothing that would let a model put a sentence in this file.

A line's sensor, if it carries one ([line.sensor], see the appraisal page), is printed beside the line as its kind and setpoint — the one thing about a charter a run's prompt does not carry — and rides in --json as sensor: {kind, setpoint} on the line, absent when there is none.

Editing through this rather than a hand-run vi buys validation feedback: a duplicate id, a typo'd table name, an unknown sensor kind or a setpoint in the wrong unit is reported the moment the editor closes, and edit exits non-zero if what you saved will not load — rather than at the next run's startup, where the warning scrolls past. Reading the file back is the ordinary loader, so what it says is what a run would get.

A charter that fails to parse exits non-zero rather than degrading to none, because every run would otherwise proceed silently un-chartered. Output distinguishes three states a naive reader would conflate: no file at all, a file with no lines yet, and a file that did not load.

mecha charter
mecha charter edit
mecha charter --json | jq -r '.lines[] | "\(.id)\t\(.text)"'
mecha charter --json | jq '.over_budget'

See Goals and appraisal.

sessions

Inspect saved transcripts. Requires a subcommand.

mecha sessions <list|show|path|stats|health|appraise> [OPTIONS]
SubcommandFlagDescription
list-n, --limit <N>How many to show. Default 20.
show<ID>Session id or unique prefix.
show--jsonEmit the raw JSONL records instead of formatted text.
path<ID>Print the path to a session file.
stats--days <N>Only sessions started in the last N days.
stats--jsonEmit JSON instead of a table — an object {rows, sessions_unreadable} (changed from a bare array in v0.1.16: the unreadable-transcript count belongs beside the rows it is missing from).
health--days <N>Only sessions started in the last N days.
health-n, --limit <N>Stop after this many sessions, newest first.
health--jsonEmit JSON instead of a table.
appraise--days <N>Only sessions started in the last N days.
appraise-n, --limit <N>Stop after this many sessions, newest first.
appraise--jsonEmit JSON instead of a table.
appraise, health--kind <KIND>Filter to a recorded surface, such as web, task, or tui.
appraise, health--include-testsInclude smoke-test sessions; --kind test implies this.
appraise--probeResolve each intervention's agency by counterfactual replay. Paid — a model run per intervention.
appraise--max-probes <N>Ceiling on replays across the whole walk. Default 25. Requires --probe.
appraise--appraiseRun the quarantined appraiser over each session's numeric evidence. Paid, and independent of --probe.
appraise--max-appraisals <N>Ceiling on appraisals driven. Default 25. Requires --appraise.

stats totals token usage — and cost, where prices are configured — grouped by provider and model. Transcripts live in ~/.mecha/sessions unless MECHA_SESSION_DIR says otherwise.

health is the other question: not what runs cost but how they went — stop causes, tool calls against errors and denials, runs that finished over a failed call, compactions taken. Rates split by model, because a blend across two describes neither, and a rate with no denominator prints rather than 0%. Transcripts written before the outcome record carry none, so the corpus fills as you use it. health also reports goal drift and null/reopened steps, with per-run rates and explicit denominators. See Run quality.

appraise is the third question: not what runs cost, nor how they went, but how they went against what they were for — the signed error per channel and the label derived from it. Nothing is stored; each appraisal is derived on the spot from transcripts, outcomes, drafts, questions, front-door records, clean correction reflections, and the charter. Read the separate positive/negative valence alongside the label counts, goal-confirmation coverage, and source readability flags. Both paid passes are off by default and are counted apart from each other in --json, where absent means did not run rather than found nothing.

--probe builds a real agent with a real workspace jail, so run it from a project directory or name one with --workspace; from a home directory it refuses, because the jail would cover ~/.mecha. See Goals and appraisal.

mecha sessions list -n 50
mecha sessions show 20260805T091500 --json | jq -r 'select(.role == "user") | .content'
mecha sessions stats --days 30
mecha sessions health --days 30
mecha sessions health --json | jq '.by_model'
mecha sessions appraise --days 30
mecha sessions appraise --days 7 --probe --max-probes 10
mecha sessions appraise --json | jq '.labels'
cat "$(mecha sessions path 20260805T091500)"

replay

Re-run a recorded session against its recorded tool results and report where the model diverged.

mecha replay [OPTIONS] <SESSION>
FlagDescription
<SESSION>Session id, unique prefix, or a path to a transcript file.
--on-divergence <stop|error|live>What to do when the replay departs from the recording. Default stop.
--jsonEmit the report as JSON instead of prose.

stop ends the run at the divergence, because after one every later recorded result answers a question nobody asked. error does the same and exits non-zero on any divergence, argument spellings included — use it in CI. live abandons the recording and keeps going against the real tools.

mecha replay 20260805T091500
mecha replay 20260805T091500 --on-divergence error --json > replay.json # CI gate
mecha replay ~/.mecha/sessions/20260805T091500-3f2a1b7c.jsonl -m qwen3-14b -p local

outbox

Review, edit, release, or reject staged outbound actions. list is the default subcommand.

mecha outbox [list|show|edit|review|approve|reconcile|reject|anticipate|outcome] [ARGS]
SubcommandFlagDescription
listList staged items, grouped by kind.
list--kind <KIND>Only message or only publish.
list--via <VIA>Only items staged by a tool whose name contains this.
show<ID> [--json]Readable draft, provenance, and edits; --json shows exact arguments.
edit<ID>Open the draft's prose in $EDITOR.
edit--jsonEdit all arguments, including recipients, as JSON.
edit--body-file <FILE>Replace prose from a file; conflicts with --json.
review[IDS]...Walk items one at a time, deciding each. Ids, or unique prefixes; several is fine.
review--allEvery pending item, subject to the filters.
review--kind <KIND>Only message or only publish.
approve<ID>Execute the item's tool call, for real, and mark it sent.
approve-y, --yesSkip the confirmation shown for items drafted in a tainted conversation.
reconcile<ID> --outcome delivered|not-delivered --evidence <TEXT>Record an uncertain delivery's observed outcome; never sends.
reject<ID>Refuse an item. It stays on file as the record of the refusal.
reject--reason <REASON>Why — recorded on the item for the next reader.
anticipate<ID> [--file <FILE>] [--guide|--observe]Read predictions or attach owner evidence to a pending message. --guide requires a current, resolved assessment before release; omitting the mode preserves it.
outcome<ID> --file <FILE>Record post-delivery owner feedback linked to the prediction; revisions explicitly supersede prior feedback.

approve is the current verb; send remains an alias. The original draft is kept after editing, and mecha reflect mines the edit into writing lessons. Delivery attempts are recorded durably. An uncertain outcome blocks retries until the owner checks the destination and reconciles it; see delivery recovery.

An item's kind decides how it is reviewed, not how it was staged. A publish shows the rendered page rather than the arguments, and refuses edit — see Publishing.

mecha outbox
mecha outbox show 3f2a
mecha outbox edit 3f2a && mecha outbox send 3f2a
mecha outbox review --all --kind message
mecha outbox reject 3f2a --reason "wrong recipient"

review

Everything waiting on a human, across every store — and the knowledge graph's merge queue, decidable from mecha. queues is the default subcommand. See The queues for the design.

mecha review [queues|proposers|list|sample|items|accept|reject] [ARGS]
SubcommandFlagDescription
queuesWhat is waiting, across every store. An unreadable store shows , never 0.
proposersThe graph queue by proposing mechanism, with each one's human accept rate and Wilson lower bound. Machine rejects reported beside the rate, never inside it.
list--proposer <P>Pending classes, optionally one mechanism's.
sample--proposer <P> --predicate <PRED>Individual candidates drawn uniformly at random — the default way to look at items, because judging the head of an ordered queue measures the ordering.
sample-n <N>, --seed <S>Sample size (default 12) and the seed. Omit the seed and one is drawn and printed, so any sample can be redrawn and checked.
items--proposer <P> --predicate <PRED>Queue order, for a class already decided about. Says outright its verdicts are not a rate.
accept[IDS]...Accept candidates by id.
accept--proposer <P> --predicate <PRED>A whole class. A cluster kind like (commitment) is refused by name rather than matching nothing.
accept--limit <N>, --dry-runCap the bulk match (the graph defaults to 500), or see what would be hit without changing anything.
rejectSame shapes as accept, plus --reason.

Every subcommand takes --json. The graph verbs drive the mecha-graph binary as a child process ($MECHA_GRAPH_BIN, else PATH) — deliberately not an MCP tool, so nothing a model can call accepts a fact candidate.

mecha review
mecha review proposers
mecha review sample --proposer bee:suggested --predicate related_to -n 12
mecha review accept --proposer linker:knn --predicate related_to --dry-run

msg

Messages between this machine's own agents — a chat session, a trigger, a one-shot run — addressed by producer name rather than by session, so an overnight trigger can write to chat without knowing which chat will read it. Delivery happens at the recipient's next turn boundary. Requires a subcommand.

mecha msg <send|list|show|dismiss|agents> [ARGS]
SubcommandFlagDescription
send<TO> <BODY>Leave a message for a producer: chat, a trigger's name, run.
send--from <NAME>Sender recorded on the message. Default user.
send--reply-to <ID>Id of the message this answers.
listMessages, pending first, across every mailbox.
list--to <NAME>Only this recipient's mailbox.
list--allInclude delivered messages, not just pending.
show<ID>One message in full. Id or unique prefix.
dismiss[IDS]...Set pending messages aside unread. Ids, or unique prefixes.
dismiss--allEvery pending message instead.
dismiss--to <NAME>With --all: only this recipient's mailbox.
agentsWhich agents are live right now, per the session markers.

The agents are wired up by [messages] enabled, which is off by default, but this surface is not gated on it: the store is yours, and "what did the overnight run tell me" must not depend on a feature flag.

dismiss rather than rm is the shape that matters — a full mailbox refuses new sends, so a backlog nobody is coming to claim needs setting aside, and the message stays on file either way.

A send from a terminal is stamped clean, because a person typing is the one sender whose words are trusted input. A send whose stdin is not a terminal — a pipe, a script, or an agent's shell reaching for mecha msg send to route around the harness — is stamped private and untrusted, so the receiver's interlock sees it exactly as message_send would have presented it.

mecha msg send chat "the briefing is in ~/.mecha/work/briefing"
mecha msg list --all
mecha msg show 9c1e
mecha msg dismiss --all --to chat
mecha msg agents

work

What runs have generated, and removing what is past. Every producer — a trigger, a chat, a session — writes into its own directory, which is also the path jail its runs get. list is the default subcommand.

mecha work [list|path|clean] [ARGS]
SubcommandFlagDescription
listWhat each producer has generated, with entry counts, size, and the newest entry.
path<PRODUCER>Print one producer's directory, creating it if absent. For cd $(mecha work path x).
clean--keep <N>How many entries survive per producer. Defaults to [work] keep (10).
clean--producer <NAME>Only this producer.
clean--dry-runSay what would go, and remove nothing.

clean never removes anything a published bundle names as a source, and says which entries survived for that reason. The producer directory itself is never removed.

mecha work
mecha work path briefing
mecha work clean --dry-run
mecha work clean --producer briefing --keep 3

See The work directory.

mail

The inbox as a queue you work. list is the default subcommand.

mecha mail [list|show|classify|reply|forward|schedule|archive|spam|task|needs-info|correct|dismiss|reflect|score|eval] [ARGS]

Threads are named by an eight-character handle — the last eight characters of the id — and any unique suffix is accepted. A suffix rather than a prefix because Outlook conversation ids share a 57-character common prefix. Ambiguity is an error, never a guess.

SubcommandFlagDescription
list--allinclude threads already acted on, and the ones classified ignore
list--agedday two: respond threads old enough to have been answered and still untouched
list--aged-hours <N>how old that means. Default 30 — a working day, so an evening email is not nagged about at breakfast
list--surfacerecord that these were surfaced, so they are not surfaced again. Deliberately separate from reading the list
list--jsonmachine output: the typed fields only
show<THREAD>read one thread — the prose, for a human
classify--account <NAME>one mailbox. Omit to sweep every configured account
classify--limit <N>recent threads to consider per account. Default 25
classify--forcere-classify threads already in the store
classify--dry-runsay what would be classified, and spend nothing
reply--note <TEXT>extra steering — "decline politely", "ask for the deadline first"
forward--to <ADDRS>comma-separated recipients
task--name / --due / --context / --projectthe task, its deadline, its GTD context (@email), and a parent project that must already exist on the graph
needs-info--missing <TEXT>what you are waiting for, in your own words
correct--bucket / --urgency / --proposed / --request-type / --deadlinefield-level; none clears a field
reflect--dry-runturn corrections into triage-domain reflections
score--min-age-hours <N>exclude threads younger than this. Default 48
eval--sample / --seed / --prefilter-only / --out <PATH>grade the classifier against a corpus whose outcome is known

Every subcommand takes --account <NAME>.

reply, forward and schedule stage into the outbox and never send. archive and spam reach nobody outside your own mailbox and so are not staged. Separate verbs rather than one --action argument, because a free-form label would put spam inside a verb that reads as harmless.

eval writes nothing to the triage store: grading year-old mail is not triaging it, and a scorecard that mutated the queue it measures would be unrepeatable. score reads the corpus written by mecha-mail corpus, not the MCP tools — a measurement keyed on a display format breaks silently the day the format changes.

mecha mail classify --account dartmouth
mecha mail list
mecha mail list --aged --surface # what the morning briefing runs
mecha mail show 3f2a1b7c
mecha mail reply 3f2a1b7c --note "decline politely"
mecha mail correct 3f2a1b7c --bucket respond --urgency today
mecha mail task 3f2a1b7c --due +3d
mecha mail reflect --dry-run
mecha-mail corpus --since 2026-07-01 --account dartmouth && mecha mail score

See Mail and calendar.

tasks

The GTD board in the knowledge graph. list is the default subcommand.

mecha tasks [list|add|set|source|work|stop|steer] [ARGS]

Reached through the MCP tool surface (kg_task_list / kg_task_create / kg_task_update), the same way the model reaches it — so this is one reader of one store rather than a second copy of it, and a configuration with no graph server says so instead of showing an empty board.

SubcommandFlagDescription
list--closedalso show done and dropped — the history
list--jsonmachine output: the tool's own answer, which is what the /tasks modal reads
add<NAME…>the task, phrased as an action. Trailing words are joined, so it needs no quoting
add--due <WHEN>YYYY-MM-DD, today, tomorrow, or +Nd
add--project <NAME>parent project — must already name a node on the graph; an unknown name is an error, not an implicit node
add--context <TAG>GTD context, e.g. @email, @lab
set<ID>the task's node id, from tasks list
set--status <S>next, inbox, scheduled, waiting, done, dropped
set--due / --defer / --context / --waiting-on / --projectOmit to leave untouched; pass "" to clear. Projects resolve by name or node ID.
source<ID>Read the source the task was captured from.
work<ID> [--note <TEXT>...]Delegate the task in its own conversation and workflow.
work--unattendedRun without terminal approval prompts; reads run and configured sends stage.
work--resume <SESSION>Continue the recorded conversation with its taint; the previous writer must release it first.
work--againStart another run on an already delegated task.
stop<ID>Request cancellation of the active task run.
steer<ID> <TEXT>...Redirect the active task run without stopping it.

A capture lands in inbox — captured, not yet committed to. done and dropped stamp a completion time and are reversible: any other status reopens the task. Nothing here deletes, and there is no delete verb, because the board is the record.

The omit-versus-empty distinction on set is the tool's and is passed through rather than reinterpreted — a driver that read "unset" as "clear" would wipe a due date every time somebody changed a status.

Closing a task appraises the run that served it. The transition into done or dropped — and only that transition, once — builds an appraisal off the session the task was delegated to and prints the verdict. A done closure whose label names residue — or whose run was cut short — may stage a follow-up task for it; a distress closure does not, because a rejected draft or a steer is a verdict you already delivered with nothing in it to put on the board; a dropped one never does, because dropping is you declining the work rather than accepting mediocre work, and proposing a follow-up there would override the decision you just made. All of it is best-effort: the status change lands whether or not the appraisal does. See Goals and appraisal.

mecha tasks
mecha tasks add --due +3d --context @lab -- Re-run the eval set on the new prefix
mecha tasks set task-1a2b3c4d --status next
mecha tasks set task-1a2b3c4d --due "" # clear it
mecha tasks list --closed

Delegation leaves the task yours to close. Its workflow tracks questions, drafts, and completion evidence across continuations.

The /tasks modal in mecha tui drives these operations, and mecha-graph tui screen 6 is the same board with the same status letters.

workflow

Track an outcome across tasks, conversations, drafts, and owner decisions. Delegating a board task creates its workflow automatically. Workflows supplement the graph task board; closing one does not close the graph task.

mecha workflow today
mecha workflow add "Prepare the grant reply" --workspace ./grant
mecha workflow show FLOW_ID
mecha workflow check FLOW_ID --artifact reply.md --contains "revised budget"
mecha workflow verify FLOW_ID
mecha workflow close FLOW_ID
CommandsPurpose
today, list, show <ID>Read priorities and workflow records.
add <TITLE>Create a record; optionally link --task and --session.
commit <ID>Record --party, --source, --due, and --follow-up; timestamps require an offset.
check, uncheck, verify, closeDefine and inspect artifact or delivery evidence before completion. uncheck <ID> <N> uses a one-based check number.
depend <ID> <PREDECESSOR>, resume <ID>Require a completed predecessor and continue the recorded conversation.
cancel <ID> --reason <TEXT>, reopen <ID>Stop tracking or explicitly reopen; cancellation does not stop an active runner.
recover <ID> --reason <TEXT>Clear stale ownership after confirming the old runner stopped.
attention, tick, snooze, ackConfigure quiet hours, refresh reminders, defer or acknowledge notices. tick --dry-run previews without writing.

See Workflows and Today for the complete walkthrough, including dependencies, commitments, and delivery recovery.

questions

Read and answer a question parked by a delegated run. Answering resumes its recorded conversation with its existing taint and approval restrictions.

mecha questions
mecha questions show QUESTION_ID
mecha questions answer QUESTION_ID "Use the revised budget"
mecha questions abandon QUESTION_ID

list is the default; list --all includes answered and abandoned questions. answer --unattended resumes without terminal prompts; actions needing approval are refused in the machine's voice. abandon resolves the question without resuming. A blocked resume leaves the question open for a later attempt.

polls

Inspect meeting-poll state and prepare the owner's pick card.

mecha polls list
mecha polls sweep
mecha polls pick POLL_ID 1

pick uses a one-based rank. sweep stages needed cards and folds reviewed outcomes into poll records; repeating it is safe. Review the chosen action in the outbox. See Polls.

kg

Read and maintain the graph through the configured kg_* MCP tools.

mecha kg notes
mecha kg search "grant planning" --k 5
mecha kg entity "Priya" --json
mecha kg note "Discussed the grant timeline with Priya."

note --edit <SOURCE_ID> rewrites an existing note; it does not retract claims already extracted from the old text. assert and retract maintain facts; alias and unalias maintain names using node IDs. related reads a bounded neighborhood; timeline includes superseded facts. See The graph and each subcommand's --help.

frontdoor

Requests that arrived through the public surface, and the quarantine they pass through before any run with tools is told about them. list is the default subcommand.

mecha frontdoor [list|show|extract|next|triage|needs-info|close] [ARGS]
SubcommandFlagDescription
listWhat has arrived, and what state each request is in.
list--state <STATE>Only this state: drained, extracted, extraction_failed, …
show<SEQ>One request in full, including the prose a stranger wrote.
extractRun the quarantined extraction over everything not yet extracted.
extract--seq <SEQ>Just this one.
extract--forceRe-extract records that already have an extraction.
next--limit <N>What a triage run may be told, as JSON — extractions only, never prose. Default 5.
triageDraft a reply to each extracted request, into the outbox.
triage--seq <SEQ>Just this one.
triage--limit <N>At most this many. Default 5.
needs-info<SEQ>Park a request until the requester answers something.
needs-info--note <TEXT>What is missing.
close<SEQ>End a request.
close--reason <REASON>Why. Requiredany → closed is the one transition that must never be silent.

The verbs split along the quarantine. list and show are for you: show is the one place the original text is printed, and a terminal is where that is safe, because you cannot be prompt-injected into sending your own calendar somewhere. extract is the quarantined pass — a tool-less model call per record, turning prose into typed fields. next is what a triage trigger pipes into a prompt, and it is structurally incapable of including the words a stranger typed.

triage is the privileged half: a full agent with mail and calendar, told only what next would print, drafting into the outbox. It refuses to run without [outbox] tools naming the send, rather than running unrouted — a stranger's inbox is not where you want to discover the route was unset. Each request gets a fresh conversation, so one request's flagged prose cannot arm the interlock for the request behind it.

needs-info and close are how a request stops growing the queue. A rejected draft returns its request to extracted rather than to closed: "not this reply" is not "not this request".

Draining is deliberately not here — mecha-factory-publish drain holds the key and speaks the protocol, and the common case, nothing new, must cost zero tokens and no model at all.

mecha frontdoor
mecha frontdoor list --state extraction_failed
mecha frontdoor show 42
mecha frontdoor extract
mecha frontdoor next --limit 3
mecha frontdoor triage --limit 3
mecha frontdoor needs-info 42 --note "no date given"
mecha frontdoor close 42 --reason "answered by the sent draft"

See The front door.

slack

Driving mecha from Slack: the credential, the binding, and who may drive. status is the default subcommand.

mecha slack [status|auth|link|threads|connect|sweep|notify|send|remote|unlink] [ARGS]
SubcommandFlagDescription
statusWhat is bound, and whether the credential still works.
authStore the bot and app-level tokens, after proving them against Slack.
link--timeout <MINUTES>Give up on an unclaimed code after this long. Default 10.
link--forceBind even though this install is already bound to another workspace.
threads--state <STATE>What state each thread is in: idle, running, awaiting_input, cancelled, staged, done, failed, orphaned.
connectRun the connector: hold the Slack socket open and drive runs from threads.
sweepMark threads whose run did not survive a restart, so none is left showing "working…" forever.
notify--title <TEXT>Read stdin and send it to the owner as a DM.
send<PATH>, --comment <TEXT>Upload a file to the owner's DM — a chart, a log, a screenshot.
remote--sweepNamed threads a TUI session is mirrored into. --sweep cools any whose session has gone.
unlinkForget the binding. The tokens stay, so link can be run again.

auth reads the tokens from MECHA_SLACK_BOT_TOKEN (xoxb-) and MECHA_SLACK_APP_TOKEN (xapp-) rather than from flags, because a flag lands in shell history and in ps output, and a Slack bot token reaches the whole workspace. It proves both against Slack before storing either.

link prints a one-time code here and binds whoever types it into Slack. Typing a code printed on this machine proves shell access to the machine the agent runs on; an email address proves only what the workspace claims about it.

connect is what the systemd unit runs (scripts/mecha-slack.service); it does a sweep on startup. notify is what a trigger's notify calls — that command already runs with the run's answer on stdin, so --notify 'mecha slack notify --title briefing' puts the morning briefing on a phone with no new trigger concept at all.

send is how something a headless box made gets looked at. Over SSH there is no viewer, and scp in the other direction is a second connection nobody wants to set up to look at a PNG — so the file goes to the one place already reachable from a phone. The destination is not an argument: it is the owner's DM, from the binding, and there is deliberately no flag that moves it. [slack] max_upload_mb caps it (25 MB by default), in both directions.

The TUI has the same thing as /send <path>, with one difference: there the path goes through the run's path jail, because a session has one and there is no reason for it to have a second rule. Here the path is taken as typed — this verb runs in your own shell, which is already the boundary.

Nothing in [slack] config grants access. Who may drive lives in ~/.mecha/slack/binding.json, a store rather than config.

export MECHA_SLACK_BOT_TOKEN=xoxb-…
export MECHA_SLACK_APP_TOKEN=xapp-…
mecha slack auth
mecha slack link # then type the printed code at the app in Slack
mecha slack status
mecha slack threads --state awaiting_input
echo "deploy finished" | mecha slack notify --title deploy
mecha slack send results/accuracy.png --comment 'the run finished'
mecha slack remote # what this machine is mirroring
mecha slack remote --sweep # cool attachments whose session died

See Slack.

trigger

Prompts that run on a schedule. list is the default subcommand.

mecha trigger [list|add|show|edit|rm|enable|disable|next|run|tick|daemon|cancel|runs] [ARGS]
SubcommandFlagDescription
listTriggers, when each next fires, and how the last run went.
add<NAME>Lowercase letters, digits, - and _. It is the filename.
add--schedule <CRON>Five-field cron, or @daily/@hourly/@weekly. Required.
add--prompt <PROMPT>What to ask. @path reads it from a file. Required.
add--description <TEXT>One line, shown under the trigger in list.
add--timezone <IANA>Defaults to [agent] timezone, and is written into the file either way.
add--timeout <DUR>Wall-clock ceiling on one run. 20m by default.
add--catch-up <SPEC>always (default), never, or a duration like 2h.
add--notify <CMD>Command run with the answer on stdin.
add--disabledCreate it switched off.
add--forceOverwrite an existing trigger of the same name.
show<NAME>The trigger's settings and its recent runs.
show--lastPrint the last run's answer, read back from its session transcript.
edit<NAME>Open the trigger's file in $EDITOR.
rm<NAME>Delete a trigger. Its ledger rows stay as the record.
enable<NAME>Let a trigger fire again.
disable<NAME>Stop it firing without deleting it or losing its history.
next[NAME], -n, --count <N>Upcoming fire times, without running anything. Default 5.
run<NAME>Run one trigger now, whatever its schedule says.
tick--dry-runSay what would fire, and fire nothing.
daemon--print-unitTick once a minute until stopped. --print-unit prints a systemd user unit naming this binary by absolute path and exits, running nothing — the path for a crates.io install, which has no scripts/ directory to copy from.
cancel<NAME>Stop the run in flight, if there is one.
runs[NAME], -n, --count <N>The run ledger, newest first. Default 20.

The schedule is five fields — minute hour day-of-month month day-of-week. Seconds are not a field: 0 7 * * * is 7am.

tick is the primitive and daemon is a loop over it, so a crontab line or a systemd timer reaches the same answer: being due is a function of the ledger and the clock. Missed slots collapse — a machine off for a week owes one run of each trigger, not a week's worth — and --catch-up decides whether a stale slot still runs, with skips written to the ledger.

trigger run is recorded with no slot, so a test run at noon does not cancel tomorrow's 07:00. trigger cancel stops the run at its next safe point, keeping the partial answer, and works even when the run is inside the daemon's process.

Triggers are read-only unless the file says otherwise; --yes at add time is what writes allow. Outbox staging still works under read-only, because staging executes nothing. Definitions live in ~/.mecha/triggers/<name>.toml and a trigger run reads ~/.mecha/config.toml only, never a mecha.toml from the directory it starts in.

mecha trigger add briefing \
--schedule '0 7 * * 1-5' \
--prompt "Summarise anything in my inbox that needs an answer today, and what's on my calendar." \
--catch-up 3h --notify 'notify-send "mecha briefing"'

mecha trigger next # when everything fires next
mecha trigger tick --dry-run # what would fire, and why
mecha trigger run briefing # fire now, without consuming the scheduled slot
mecha trigger show briefing --last # the answer it produced
mecha trigger daemon # or point a systemd timer at `mecha trigger tick`

reflect

Mine recorded sessions for user interventions — a mid-run steer, a denied tool call, a corrective follow-up — and turn each into one reflection.

mecha reflect [OPTIONS]
FlagDescription
--sessions-dir <DIR>Directory of session transcripts. Defaults to the standard location.
--dry-runList what would be mined without calling a model or writing anything.
--limit <N>Mine at most this many sessions this run.

Reflections are appended to reflections.jsonl in the learning store, each carrying the session id that proves it and an Origin classified from the transcript's recorded taint. A session whose reflections fail is left unmined for a later run to retry rather than marked and silently lost.

mecha reflect --dry-run
mecha reflect -p local --limit 20

reflections

Read the lessons before anything consolidates them. list is the default subcommand.

mecha reflections [list|show|edit|drop|restore] [ARGS]
SubcommandFlagDescription
list--domain <D>Only behavior, writing or triage.
list--allInclude dropped ones, hidden by default.
list--jsonMachine-readable, for the /learning modal.
show<ID> [--json]One reflection in full: what was happening, what was said, the lesson, and whether it can become a rule.
edit<ID> [--text <T>]Rewrite the lesson in your own words. Without --text, opens $EDITOR on the lesson alone.
drop<ID> [--reason <R>]Refuse one — kept as evidence, never a candidate again.
restore<ID>Undo a drop.

The store had no reader before this, which was the wrong end of the pipeline to be blind at: a rule is a consolidation of several lessons, so by the time a proposal is reviewable the thing to disagree with has already been merged with four others and rewritten. The lesson is where a disagreement is cheap and precise.

edit is a provenance promotion, not a text change — a lesson you typed yourself skips the model that would otherwise have laundered third-party bytes into it, which is how an excluded reflection gets rescued. drop is a flag and never a deletion, on the same rule retired rules and resolved outbox items follow: a store that forgets its refusals lets the same lesson return next pass with nothing to say it was already judged.

Nothing here calls a model or touches the network, and every write takes the store lock, so it is safe to run against a store the nightly is also using.

mecha reflections
mecha reflections --domain writing --all
mecha reflections show 20260828T0915
mecha reflections edit 20260828T0915
mecha reflections drop 20260828T0915 --reason "specific to one thread"

See Learning.

learn

Absorb unprocessed reflections into the consolidated learned rule set.

mecha learn [OPTIONS]
FlagDescription
--min <N>Only run when a domain has at least this many unprocessed reflections. Default 3.
--holdout <F>Hold out this fraction of unprocessed reflections from the pass. Default 0.
--proposeMeasure the candidate and stage a surviving proposal for review.
--autoMeasure and apply unless a probe regresses; ungradeable candidates apply on probation. Conflicts with --propose.
--dry-runShow what would run without calling a model or writing anything.

learn rewrites rules/<domain>.learned.toml within a fixed character budget; rules/<domain>.user.toml is yours and is never written by code. The store is a git repo, so git log is the learning history and git revert is the undo. Non-clean reflections are excluded structurally, before any prompt is built.

--holdout is deterministic (every k-th by id), because a measurement set that changes between runs measures nothing. --propose gates the candidate by counterfactual replay first and stages what survives for mecha proposals.

mecha learn --dry-run
mecha learn --holdout 0.25 # leave a measurement set for validate
mecha learn --auto --holdout 0.25 -p local # the supplied automation
mecha learn --propose -p local # require owner review

learning-report

Read correction trends, rule health, and consolidation history without a model call or network access.

mecha learning-report
mecha learning-report --json

--bucket-days <N> controls the trend bucket width (default 7).

Use it alongside mecha rules to distinguish rules that improved measured work from rules that have not yet been measured. See Learning.

validate

Probe whether the learned rules change the answers at the recorded moments the user stepped in, and append every outcome to the validation ledger.

mecha validate [OPTIONS]
FlagDescription
--unprocessed-onlyOnly validate reflections not yet consumed by a learn pass — the held-out set.
--trigger <LIST>Probe only these triggers (comma-separated: steer, denial, followup). Default is all three.
--judge-model <MODEL>Judge model id.
--judge-provider <PROVIDER>Provider entry the judge runs on. Defaults to the model under test.
--no-attributeSkip the bisection that attributes a regression to one rule. Regressions are still recorded, just unattributed.

Steer and denial probes are counterfactual replays: the recorded prefix is driven again, with and without the rules, and the verdict is structural. Followup probes are judge-graded, so n=1 means little — read the answers before believing a flip. Run validate before learn, or the rules are graded on their own training data.

mecha validate --unprocessed-only
mecha validate --trigger steer,denial --judge-provider anthropic

rules

Rule tenure: ledger tallies per rule, retirement, and staging retirements for rules the validation ledger keeps convicting. list is the default subcommand.

mecha rules [list|retire|restore|propose-retirements] [ARGS]
SubcommandFlagDescription
listEvery rule with its ledger tallies and staleness.
retire<ID>Retire a rule by id or unique prefix.
retire--reason <REASON>Recorded on the rule and shown to the learner so the lesson does not come back reworded.
restore<ID>Un-retire a rule by id or unique prefix.
propose-retirements--min-attributed <N>Attributed regressions required. Default 3; probationary rules use the shorter threshold of 2.
propose-retirements--applyApply measured retirements or scope narrowing directly instead of staging a proposal.

Retirement is a flag, never a deletion: the rule stays in the file as evidence and rules restore undoes it. propose-retirements is a deterministic ledger scan with no model anywhere; what it stages goes through the same proposal gate as any other rule change.

mecha rules
mecha rules retire 7c1e --reason "measured harmful on the audit probes"
mecha rules propose-retirements --min-attributed 3

proposals

Review, accept, or reject rule changes staged by mecha learn --propose or mecha rules propose-retirements. list is the default subcommand.

mecha proposals [list|show|accept|reject] [ARGS]
SubcommandFlagDescription
listList proposals.
show<ID>The rules diff and the gate's evidence.
accept<ID>Apply a pending proposal to the live rules.
accept--forceApply even though the live rules changed since the proposal was measured.
reject<ID>Refuse a pending proposal, consuming its reflections.
reject--reason <REASON>Why — recorded on the proposal for the next reader.

accept checks that the live rules still match what the candidate was measured against; a diff on screen that is not the change being applied needs --force to say so. reject retires the reflections so a human's no is not re-argued nightly. Proposals can only ever touch rules/*.learned.toml.

mecha proposals
mecha proposals show 9a4
mecha proposals accept 9a4
mecha proposals reject 9a4 --reason "the second rule contradicts the first"

distill

Summarise closed sessions into episodes staged to a knowledge-graph MCP server.

mecha distill [OPTIONS]
FlagDescription
--sessions-dir <DIR>Directory of session transcripts. Defaults to the standard location.
--server <SERVER>The [[mcp]] server holding the knowledge graph. Default graph.
--dry-runList what would be distilled without calling a model or writing.
--limit <N>Distill at most this many sessions this run.

Episodes are pushed through the server's kg_upsert as evidence, not belief: the graph's own extractor turns them into candidates that wait in the user's review queue. A tainted session still distills — losing the record of a real afternoon because a web page was open would gut the memory — and the taint snapshot is recorded on the episode's metadata instead. Idempotent at both ends.

mecha distill --dry-run
mecha distill -p local --limit 10 --server graph

setup

What this install still needs, and the one command that fixes each.

mecha setup [--json] [--write] [--undecline <STEP_ID>]
FlagDescription
--jsonPrint the plan as JSON and exit. Never prompts, even at a terminal.
--writeRewrite the local provider's model, context_window and vision from what its server reports.
--undecline <STEP_ID>Ask about a step you said never to again. all clears every one. Says so when the id was never declined, rather than reporting an undo it did not perform.

The three are mutually exclusive: each is a different verb, and a pair used to resolve by whichever branch came first — --json --write printed a plan, exited 1 and wrote nothing. The parser refuses the combination instead.

Where it differs from doctor, and why both exist: doctor answers what is silently broken about a working install, in one pass with no network and no model. Every question setup asks needs to ask a server something, and its answers change a config file. Folding them together would put a network call inside the one command whose whole contract is that it has none.

For a local provider it reads GET /props and compares three settings against what is actually being served — and each is one nothing can check afterwards, because each degrades quietly rather than failing:

SettingWrong how
context_windownaming -c rather than the per-slot -c / -np, so a run compacts at a threshold nobody chose
visionunset against a multimodal server, so every image arrives as a line of text
modelnaming weights llama-server is not serving, so every session record and scorecard says the wrong thing answered

--write rewrites those three from what the server reports. It edits the table in place, preserving comments — a round trip through a TOML parser is shorter and discards them, and in a config the comments are usually why a number is what it is. It asks first at a terminal and refuses to act when nothing is watching; the previous file is kept as config.toml.bak.

The step that blocks every other one

A provider that cannot answer makes everything below it untestable, so it is reported first — and it carries a way out, which it did not always: the remedy used to be mecha config show, a command that displays a file and fixes nothing. There are two ways out and they are not symmetric, so the step says which one your machine is in.

Something is already serving. If nothing in the config can answer, setup probes http://127.0.0.1:8080 — loopback only, one address, and only on an install that is otherwise stuck, so a working install makes no extra call and nothing ever leaves the machine. When a server answers there and no provider names it, mecha setup --write writes the table from what the server reports about itself and points default_provider at it. Every value is read back off /props, so the existence of the provider is as much a measured fact as its context window.

Nothing is serving. Then the fix is an API key, and a key is the one thing this tool will not write. mecha stores the name of an environment variable, never a secret, which is what makes a config file safe to read, copy and commit. So the step names the exact variable and both routes forward rather than offering a command that could only print what you already know. A provider configured with no api_key_env at all is told that, instead of being told to set a variable it does not name.

Everything else

It also inventories the integrations — mail, documents, Slack, the knowledge graph — reporting each as ok, not set up, or unknown, and offering the next command for the ones it can. And it offers mecha config init when there is no config file: Config::load_global tolerating its absence is right, since mecha must work before anybody has written one, and is also exactly why nobody ever learned about the file that every other step is fixed by editing. Unknown is deliberately not "not set up": a credential store that could not be read offers nothing, because telling someone their mail is unconfigured when it is merely unreadable sends them through an OAuth flow they did not need.

Two things it will not do. The knowledge graph's own sources are named and never driven — mecha reaches the graph through its MCP tools and nothing else, and spawning mecha-graph source would be exactly the coupling that rule prevents. And nothing is scheduled: a trigger runner is offered only once a trigger already exists, and it is the runner, never a schedule.

It also offers the charter — the ranked standing priorities every run carries. That step exists because nothing else named the feature to a new install: doctor correctly says nothing about a charter that has never been written, since not having one is not a fault, so the only ways to discover it were the TUI's /help and the web settings gear. The remedy hands over $EDITOR; nothing here composes a priority.

Saying no, and meaning it

Each offer takes y, N, or never. N is not today and comes back next time; never records the step id in ~/.mecha/setup-declined.json, and it then reports as you said no thanks rather than as missing.

That distinction is the point: a declined step is not outstanding, so an install whose every open question has been answered exits 0 and mecha setup works as your own health check instead of being permanently red over choices you already made. --undecline is printed alongside, so the way back is never something to go looking for.

Three guarantees on it, each structural rather than a matter of the prompt being careful:

  • Only genuinely optional things are declinable — the four integrations and the charter. A provider that cannot answer is not a feature going unused, and declining it would report Nothing outstanding. on an install that cannot answer a prompt.
  • A decline never hides a fault or a fact. It applies only to a step that is missing: something wrong stays wrong (declining mail is not declining to be told your mail is broken), something unknown stays unknown, and something you later set up anyway reads as done — where the machine's state and your recorded preference disagree, the state wins.
  • The gate is on the plan, not the prompt, so hand-editing the file in cannot decline a credential either.

An unreadable decline store is reported as a read failure and everything is offered again, rather than being silently treated as "you have declined nothing".

--json prints the plan and never prompts. Exit 1 when anything is outstanding, like doctor, so a script can act on it.

doctor

Read every store — no network, no model, no tokens — and report what is silently wrong: dead mail logins, stuck outbox drafts, stalled frontdoor requests, triggers whose slots stopped advancing, failed mecha-* units, graph nightlies that stopped writing their log, and the population signals in the run corpus.

mecha doctor [--json]
FlagDescription
--jsonMachine output: the findings as JSON. Never prompts, even on a TTY.

On a terminal, each finding offers its remedy through an existing command — one at a time, EOF is no. Piped or --json, it only reports. Exit 0 is healthy; 1 means findings. Findings propose; a human disposes — there is deliberately no --yes.

Beyond the incident checks it reads populations, per model, over the last 200 sessions and only above a floor of 20 runs: a model finishing a fifth of its runs over a failed call, failing a quarter of its tool calls, or having a quarter of its runs cut short by a ceiling. Thresholds are deliberately high — a doctor that cries wolf stops being read — and cancellations are excluded, because a person pressing Ctrl-C is the system working. Two trigger checks sit beside them: one quietly failing a third of its tool calls, and one whose most recent run succeeded having made none at all. See Run quality.

diagnose

Read the run corpus and propose one change to try — the stage between doctor saying something is wrong and eval --ab-config saying whether a fix helped.

mecha diagnose [OPTIONS]
FlagDescription
--model <MODEL>Which model's runs to diagnose. Defaults to whichever has the most.
--days <N>Only sessions started in the last N days.
-n, --limit <N>Stop after this many sessions, newest first. Default 200.
--dry-runPrint the brief the diagnostician would be handed, and stop.

It proposes; it does not measure and does not apply. It prints a typed block — class, change, predicted metric, rationale — and then the exact mecha eval --ab-config … line that would falsify it, shell-quoted because the change is model-authored and the line exists to be pasted.

The brief is built from counters and doctor's own findings; there is no field for a transcript excerpt, so the corpus cannot be an injection surface. The run is read-only with learned rules and the outbox off. A proposal that reproduces eight consecutive words from anything the diagnostician read is refused — a conclusion drawn from a source is a proposal, a sentence lifted from one is the source's. Declining to propose is a legitimate answer, and a block that could not be measured parses as nothing.

mecha diagnose --dry-run # see the evidence, pay nothing
mecha diagnose --model qwen3-moe --days 14

harness

The self-improvement loop over the harness itself, and the review surface for what it did. Requires a subcommand.

mecha harness <ruminate|list|show|accept|reject|revert|overrides> [OPTIONS]
SubcommandFlagDescription
ruminate--sessions <N>Replay this many recent sessions per arm. Default 16.
ruminate--days <N>Only diagnose from sessions started in the last N days.
ruminate-n, --limit <N>Scan at most this many sessions for the corpus. Default 200.
ruminate--holdout-in <N>One episode in this many is held out of selection. Default 3, minimum 2.
list--allThe whole record, not just what is waiting on you.
list--jsonMachine-readable, for /queues.
show<ID>One candidate, whole: prediction, measurement, evidence.
accept<ID>Accept a staged candidate.
reject<ID> --reason <TEXT>Reject one, with a reason the record keeps.
revert<ID|KEY>Take an accepted override back out.
overridesThe active override layer, with where each entry came from.

ruminate is the nightly verb — diagnose, record, measure by replay, dispose — and it exits 0 on "nothing to do", because a skipped night is not a failed night. A config change that wins on selection, is confirmed on the holdout and holds the work guardrail auto-accepts into the override layer; accept on anything else only marks the record, since the change itself is yours to make. A security-class proposal is never measured and never applied.

revert puts the key back to whatever your config says and keeps the candidate record as evidence.

mecha harness ruminate --days 7
mecha harness list
mecha harness show hc-20260828T0330
mecha harness overrides
mecha harness revert max_turns

See Run quality.

vet

Judge queued knowledge-graph claims against the evidence they were extracted from, and file the verdicts beside them.

mecha vet [OPTIONS]
FlagDescription
--proposer <P>Proposer of the class to work, e.g. llm. Default llm.
--predicate <P>Predicate of the class to work, e.g. has. Default has.
--limit <N>Candidates to judge, oldest first. Default 10.
--recordFile the verdicts beside their candidates (mechanism verification).
--server <S>The [[mcp]] server holding the graph. Default graph.
--out <PATH>Write the judgements to a file as well.

A verdict is an opinion filed beside a candidate that stays pending — the graph's own review remains the door. The per-class verdict history is what its autonomy ladder promotes on.

corroborate

Judge whether queued generalisations hold beyond their one source.

mecha corroborate [OPTIONS]
FlagDescription
--proposer <P> / --predicate <P> / --limit <N> / --server <S>As in vet.
--since <DATE>Evidence on/after this date.
--min-coverage <N>Minimum episodes a source needs before it counts.

gossip

Two readers with different sources ask each other about one entity — disagreement between them is the finding, not a failure to converge.

mecha gossip --entity <ENTITY> [OPTIONS]
FlagDescription
--entity <E>The person or project to gossip about — a name, alias, or id.
--rounds <N>Rounds of question-and-answer. Bounded on purpose: a preserved disagreement is a finding.
--since <DATE>Evidence on/after this date — both readers get the same window, so a difference must be the sources disagreeing, not the world having moved.
--min-coverage <N>Minimum episodes a source needs before it can be a vantage. Default 3.
--verify <N>Claims to audit after the exchange; 0 skips the audit.
--adjudicate <N>Pending claims about this entity to adjudicate after the exchange; 0 skips it.
--server <S>The [[mcp]] server holding the graph. Default graph.

After building context on the entity, the run judges that entity's pending claims — the one output that makes the review backlog smaller rather than larger. Each reader's own searches are marked as instrumentation, so a probe cannot manufacture its own demand signal.

config

Show or create configuration. Requires a subcommand.

mecha config <show|path|init> [OPTIONS]
SubcommandFlagDescription
showPrint the merged configuration as TOML.
pathPrint the files that are being read, and whether they exist.
init--projectWrite ./mecha.toml instead of ~/.mecha/config.toml.
init--forceOverwrite an existing file.
mecha config init # ~/.mecha/config.toml
mecha config init --project # ./mecha.toml
mecha config path
mecha config show | grep -A4 '\[sandbox\]'

See the configuration reference for every key.