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-code-toolWithhold code, code mode's program tool, even with [features] code on. mecha eval forces it.
--no-step-escalationDisable quarantined checks of ambiguous completed plan steps.
--no-step-checksDo not execute declared plan checks.
--no-goal-guidanceDisable goal and charter planning guidance.
--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>Confirm what this run serves: task:<id>, project:<id>, charter:<id> or setpoint:<id>. Overrides the saved anchor when resuming; omitting it preserves the anchor.
--appraisal-evidence <FILE>Owner-authored commitment/check evidence for the matching --goal; see anticipatory appraisal.
--mismatch-case <FILE>Owner-authored JSON fixture for isolated artifact validation of later mismatches; mecha validate --trigger mismatch reads it back. Conflicts with --resume, --no-session, --image and --appraisal-evidence.
--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
/helpThis list.
/toolsTools this agent can call.
/skillsProcedures this agent can load, and which are loaded.
/charterStanding priorities, ranked — e edits them in $EDITOR.
/triggersScheduled prompts: see, edit, run, cancel.
/queuesEvery store waiting on you, incl. the graph merge queue.
/learningReflections, rules and proposals — read, edit, refuse.
/outboxStaged outbound drafts: read, edit, send, reject.
/mailThe classified inbox: read, dismiss, make a task (alias /inbox).
/frontdoorInbound requests: read, extract, triage, close.
/tasksThe graph's task board: see, capture, edit, move on.
/note <text>Capture a note into the knowledge graph.
/find [query]Search the graph: entities, facts, episodes.
/entityWho is who in the graph: rename, alias, add a person.
/pollsOpen polls on the gate: tallies, close, export.
/doctorWhat is silently wrong across the stores, and the way out.
/docsDocuments in scope, and how to put one there.
/review [now|later|auto]What happens when a run stages drafts.
/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.
/send <path>Send a file to your Slack DM, to look at elsewhere.
/remote-control [name|off]Mirror this session into a named Slack thread. Also /remote and /rc.
/exitQuit (also /quit, /q).

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. It refuses to start unless [features] web = true — an absent switch is off, and mecha features enable web writes it — and unless [web] owner_login is set 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>Override [voice] voice_port for this run: the mounted voice facade's port; 0 disables. mecha features reports [voice], not a run's flags.
--voice-yesAllow voice calls without per-call approval; configured outbound actions still stage.
--offer-target <URL>Override [voice] offer_target for this run: the voice worker's offer endpoint, on this machine. Empty means serve doesn't reach the worker: no call relay, Listen or voice previews.
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. Refuses unless [features] voice = true. The normal browser setup mounts this inside mecha serve; use a separate process when that separation is needed. The transcription and speech stack runs beside it: on Linux, mecha features enable voice installs it, and a machine it does not fit is told why and keeps the switch, for servers you run yourself.

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 and --ab-rules. 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 off the whole closed lever set (harness::Lever) — MCP, learned rules, hooks, the outbox, fallback, messages, skills, the charter, the compact tool, step escalation, step checks, goal guidance, boredom, compaction validation, predictive compaction and carried state — so a scorecard grades the model it names rather than this machine's local setup. --mcp and --ab-rules are the two opt-ins back. The approval rules file is the one lever that set's bare arm never throws; eval lifts it by its own explicit line, because a forbid in this machine's rules would score a case's shell call as blocked here and not elsewhere, and the fixture workspace is what makes lifting it defensible.

--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 report assistant-follow-through # per arm, per task, pass^k, cost; --json
mecha exp export assistant-follow-through
SubcommandFlagDescription
new<MANIFEST>Create an experiment from a TOML manifest; its name names the experiment. The design is written once, before anything runs.
run<NAME>Drive every trial the design calls for that has not finished.
run--jobs <N>Trials in flight at once, never two of one arm (they share its home). Default 1. Above 1, each trial holds one of the background model seats and waits when none is free. single only.
run--limit <N>Stop after this many trials this invocation.
run--dry-runPlan and print the trials without spawning anything.
status<NAME> [--json]Where the trials stand, per arm.
judge<NAME> [--json]Judge every treatment arm against the control through the gate.
report<NAME> [--json]Per arm, per task, and for a lifetime the pass rate along the sequence.
export<NAME>The whole record — manifest, trials, judgements — as one JSON.

new refuses an existing name. run resumes unfinished trials and skips finished ones; a trial found running at start crashed with its runner and is rerun. 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. Only a person edits a charter, and mecha charter edit is how — never a model.

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. Refused alongside edit, which has no JSON to emit.
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 charter 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 The charter.

sessions​

Inspect saved transcripts. Requires a subcommand.

mecha sessions <list|show|path|stats|health|appraise|compare> [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.
list, appraise, health--kind <KIND>Only sessions opened through this surface: run, chat, tui, web, voice, task, trigger, frontdoor, mail, slack, test, experiment or driven. A transcript from before kinds were recorded matches no filter. Driven sessions (MECHA_SESSION_KIND=driven, another agent's runs on real work) are hidden unless asked for this way.
list, appraise, health--include-testsInclude smoke-test sessions (MECHA_SESSION_KIND=test); --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<session>Print that session's text appraisal with its taint label; --text prints every one on record, and --json gives the records.
appraise--appraiseRetired. A no-op that says so on stderr; the text appraisal mecha distill writes replaced the counts-only appraiser. Kept so scripts still run.
appraise--max-appraisals <N>Retired with --appraise; accepted and ignored.
compare--points <N>Most decision points to drive this pass. Default 8. A point no structural validator can pose drives nothing and is not counted. Paid — up to three replays per point, on the local model only.
compare--seed <N>Seed for the draw of points, which is shuffled and then ordered by replay priority, so the seed decides among equals. Defaults to today's day number, and is printed.
compare--days <N>, -n/--limit <N>, --kind <KIND>, --include-tests, --jsonAs for 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, and each sensored charter line's level beside its per-item reading (charter_readings in --json). 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. The one paid pass, --probe, is off by default; in --json its probe object is null when it did not run, which is a different fact from finding nothing. appraiser is always null: that pass is retired, not merely skipped.

--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 the paid pass.

compare asks a narrower question at the moments you already answered: at a steer, a denial, a failed check, a draft you rewrote or rejected, or a surprise, would the prompt the run carried, today's learned rules, or no rules have done what you decided? Each point's policies are replayed a few turns from it and graded by your recorded act, and one comparison per point lands in the comparison store — clean sessions only. A point nothing structural can grade is stored inconclusive and never judged by a model. It refuses a provider that is not on this machine, and, like --probe, needs a project directory. See Point-wise comparison.

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'
mecha sessions compare --points 4 --json | jq '.pass'
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 keeps going against the real tools, but only through unguarded pure reads. It stops in front of the first call that your outbox routes, that an approval rule or a pre_tool hook covers, or that writes, sends or starts a subagent, and reports that call and its arguments (halted_before in --json). Nothing it stops in front of is executed, and a stop there exits 0: the call is the result.

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.
edit--args-file <FILE>Replace all arguments with a file's JSON object — --json without the editor; the web outbox's event editor writes through it.
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.
review--via <VIA>Only items staged by a tool whose name contains this.
approve[IDS]...Execute each item's tool call, for real, and mark it sent. Ids, or unique prefixes; several is fine.
approve--all / --kind <KIND> / --via <VIA>Every pending item, narrowed by the same filters as review.
approve-y, --yesThe global flag: skip the confirmation. Without it, a batch of more than one item is confirmed as a batch, and any item drafted in a tainted conversation is printed in full and confirmed; a single untainted item is not asked about.
reconcile<ID> --outcome delivered|not-delivered --evidence <TEXT>Record an uncertain delivery's observed outcome; never sends.
reject[IDS]...Refuse items. They stay on file as the record of the refusal.
reject--all / --kind <KIND> / --via <VIA>Every pending item, narrowed by the same filters as review.
reject--reason <REASON>Why — recorded on each 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 when you made every edit and the approval yourself; show names who edited (edited since drafting (by owner)), and an edit a mecha run's shell made says so when it is saved. 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|list|proposers|shadow|sample|items|groups|bind|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 graph fact candidates, optionally one mechanism's.
list--limit <N>Default 200.
shadowThe graph's surfaced-verdict queue (review-on-use): live shadow facts about to matter — contradicting a reviewed fact, served in a context pack, or spot-checked by a sampled class. Listing is the default.
shadow--confirm <FACT_UID> / --refute <FACT_UID>Decide one: confirm that a human stands behind it, or refute it as never true. The two conflict.
shadow--reason <R>, --limit <N>Why, for --refute (feeds the graph's rejection memory); how many to list, default 10.
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.
items--limit <N>Default 50.
items--ids <IDS>Only these candidate ids, comma-separated, in the order given — how a similarity group's members are read in full.
groups--proposer <P> --predicate <PRED>One class's pending candidates grouped by semantic similarity, largest first — where one verdict fans out furthest. A group's face is a real member plus samples, never a model-written summary. Commitments do not group.
groups--allGroup the whole pending queue regardless of class, at the graph's stricter global floor; each group names the classes it spans. --proposer becomes an optional filter.
groups--threshold <F>Cosine floor; omitted, the graph's calibrated default for the mode applies.
bind<ID> [--to <NAME>]Rebind a candidate's unresolvable subject to a real entity — the way through cannot resolve subject 'X'. --to is the target's exact display name, else the graph's top suggestion. The old spelling is learned as an alias.
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.
accept--create-subjectsA subject the graph does not know becomes a new topic node instead of a failure.
accept--like, --threshold <F>Cascade: the one named id is your verdict, and every same-class candidate similar to it follows as a machine cascade the autonomy ladder never counts. --threshold is the cosine floor.
accept--cascade <IDS>, --across-classesCascade over an explicit comma-separated member list from a groups listing; --across-classes lets those ids span classes (pair with groups --all).
rejectSame shapes as accept except --create-subjects, plus --reason.

queues, proposers, list, shadow, sample, items and groups take --json; bind, accept and reject do not. 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.

mecha msg follows the messages switch ([features] messages, the same switch as [messages] enabled), which is off by default: with it off, every subcommand refuses with mecha features enable messages. Nothing new arrives while it is off — no session has the mailbox tools — and what is already in the store stays there for when it is on.

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). Conversations (web, persona, Slack) are not counted.
clean--retain <forever|DURATION>How long a conversation's workspace outlives its newest file. Defaults to [work] retain_conversations (forever).
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.

document​

Extract a PDF from the terminal: each page's own text layer and, for pages with none (a scan) or with --mode ocr|both, a local OCR model's transcript. A picture of text — PNG, JPEG, WebP or GIF — is one page, read by OCR only; HEIC and TIFF are refused with the format to convert to. Needs [features] documents = true (mecha features enable documents) and a [documents] table in ~/.mecha/config.toml. Shares the cache with the document_read tool, so a page read here is not transcribed again.

mecha document extract <FILE> [--pages <SPEC>] [--mode auto|text|ocr|both] [--json] [--no-cache]
mecha document prune [--days <N>]
mecha document forget <FILE|SHA256>
SubcommandFlagDescription
extract--pages <SPEC>all (default), 3, 2-5, 1,4,7-9.
extract--mode <MODE>auto (default): the text layer, OCR only where a page has none.
extract--jsonThe whole extraction, including each page's text-layer regions with their boxes in PDF points.
extract--no-cacheNeither read nor write the cache.
prune--days <N>Remove cached extractions not read for N days. Defaults to [documents] cache_days.
forget<FILE|SHA256>Remove one file's cached extraction.

extract exits non-zero when any page could not be transcribed, after printing the rest, so a script never mistakes a partial extraction for a whole one.

mail​

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

mecha mail [list|show|classify|recent|calendars|compose|dismiss|correct|reply|forward|schedule|archive|spam|task|needs-info|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
recent--max <N>the plain inbox, untriaged: the most recent messages across every account (or one), newest first, through the same mail_recent tool the model uses. Default 15
recent--jsonpass the tool's rows through
compose--to / --subject / --body / --ccwrite a new email. --body is Markdown, converted to HTML at send time. No model runs: your words are staged verbatim
reply--note <TEXT>extra steering — "decline politely", "ask for the deadline first"
forward--to <ADDRS>comma-separated recipients
schedule--note <TEXT>put what the thread announces on your own calendar. Nobody is invited unless the note names them — "invite Priya too"
calendars--account <NAME> / --jsonevery account's calendars, with write access noted
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
score, eval--account <NAME>which account's corpus. Default: the one mecha-mail sends from (default_mail, else default)
eval--sample / --seed / --prefilter-only / --out <PATH>grade the classifier against a corpus whose outcome is known

Every subcommand except list takes --account <NAME>; list reads the triage store across every account. On compose, omitting it sends from the default account, and the send fails loudly at release if none is set.

compose, reply, forward and schedule stage into the outbox and never send — a draft you typed yourself waits in the same queue as one a model wrote. 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 campus
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 campus && mecha mail score --account campus

See Mail and calendar.

mail rule​

Owner-managed mail rules: add, list, show, mode, test, ledger, undo, and pause-all. New rules install in shadow. Mode changes name an action group; automatic permission requires owner approval of the current files. File changes lapse that permission. list, show, and ledger accept --json and remain readable with the feature off, as do revocation and pause-all. test reads 30 days of mail without writing; undo restores an exact mailbox message and records the correction.

Enable unattended execution explicitly, then install the systemd user units as with the classify timers:

cargo install --path mecha-mail
mecha features enable mail mail_rules
mkdir -p ~/.config/systemd/user
cp scripts/mecha-mail-rules.{service,timer} ~/.config/systemd/user/
cp scripts/mecha-mail-classify.service scripts/mecha-mail-classify-day.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now mecha-mail-rules.timer

The timer runs mecha-mail rules apply --headers every five minutes without a model. At the end of non-dry classification, --after-classify processes verdict-dependent and deferred work for the same accounts. Hook errors are reported and counted on stderr without changing classify's result. A hung hook has a ten-minute total allowance, reserving its last twelve seconds for SIGTERM, a ten-second grace, then SIGKILL and bounded reaping if needed. Re-copy both classify services when upgrading: their timeouts reserve ten minutes for the hook (40 minutes daytime, 55 nightly); replacing the binary alone is insufficient. An existing apply lease skips successfully; typed apply failures return non-zero, while held work does not. Install mecha-mail beside mecha: the child launcher does not search PATH. These timers currently have no launchd twin.

mecha doctor reports unreadable stores, lapsed or corrupt approvals, unknown outcomes, stranded/recovered pending actions, missing account addresses, standing provider holds and enabled timer problems. Every automatic group under unconfined shell gets attention: configure the sandbox to protect the store. mecha review and /queues count rule proposals awaiting install and lapsed group approvals; proposal age currently uses file modification time.

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.
set--session <SESSION>The conversation working this task. Set by the harness that starts one, never typed by a person.
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 Appraisal reference.

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, and block further runs of the workflow's task — tasks work, a web chat on it, an answered question resuming it — until explicitly reopened. Cancelling does not claim completion, does not stop an active runner, and does not affect triggers, which are not tied to a task.
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|search|entity|assert|retract|related|timeline|alias|unalias|note> [ARGS]
SubcommandFlagDescription
notes--limit <N>, --jsonRecent notes, newest first. Default 20.
search<QUERY>..., -k <K>, --jsonThe same context pack the model gets; #tag tokens filter to hand-tagged episodes. Default 10 results.
entity<NAME>..., --jsonEverything about one entity: facts, interaction recency, episodes. --json prints the node id and fact uids the other verbs take.
assert<SUBJECT> <PREDICATE> [OBJECT]State a fact yourself. Lands live, never in the review queue. A connection is a fact too, so this is how a node gains one.
assert--value <VALUE>A literal object, when the object is not a node; omit OBJECT.
assert--statement <TEXT>The sentence form search and readers see. Composed from the parts when omitted.
retract<UID>Retract a fact by uid — never a text match.
retract--as-of <WHEN>When it stopped being true. Omitted, it is invalidated as of now — right for a claim that was never true.
related<NAME>..., --hops <1|2>, --jsonThe bounded neighborhood over current facts. Default 1 hop.
timeline<NAME>..., --jsonSuperseded facts beside what replaced them, and the episode timeline.
alias / unalias<NODE_ID> <ALIAS>Add or remove an alias, by node id rather than name.
note<TEXT>..., --edit <SOURCE_ID>Capture a note as an episode, or rewrite an existing one in place.
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 and re-mines it; it does not retract claims already extracted from the old text. 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. Required — any → 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.
--remine-untrustedOne-shot backfill: re-mine sessions whose reflections the provenance gate excluded, through the clean-evidence path — the user's own words and tool names, tainted excerpts withheld. Idempotent.
--backfill-situationsOne-shot backfill: give reflections mined before the situation field one, recomputed from their transcripts with no model call. Idempotent; --dry-run reports what would be written.

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.
--compare-sourcesLearn nothing: measure the reflector's lessons against the text appraisal's on the same interventions and report per region (below). Conflicts with --propose, --auto, --dry-run and --holdout.
--interventions <N>With --compare-sources: most interventions to replay this pass. Default 8.
--seed <N>With --compare-sources: seed for the uniform draw. Defaults to today's day number, and is printed.
--jsonWith --compare-sources: the pass's tally and the report as JSON.

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 plain files: runs.jsonl and passes.jsonl are the learning history, and mecha rules retire / mecha rules restore are the undo, one rule at a time. A store created before 2026-09-28 may still carry a .git directory; nothing commits to it any more, so never git revert there — it would rewind the store to a stale state. 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 # the supplied automation
mecha learn --propose # require owner review
mecha learn --compare-sources # measure lessons by source; learns nothing

--compare-sources is a measurement and writes no rule, proposal or validation row — only one comparison per intervention in the comparison store. It replays each drawn steer or denial the reflector reflected on three times — no rules, the reflector's lesson, and the lessons of that session's text appraisal — holding one background model seat, on a provider on this machine only. See Lessons by source.

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, mismatch). Default is all four. Mismatch probes need an owner-supplied artifact fixture (run --mismatch-case).
--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.
--repeatRe-run unchanged probe inputs deliberately. By default only fresh inputs, transient failures, and regressions awaiting confirmation run again.
--cover <N>Also probe up to this many reflections per (rule, region) pair the ledger has never graded, so a widened rule is probed in each sub-region. Default 0.

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|show|retire|restore|propose-retirements] [ARGS]
SubcommandFlagDescription
listEvery rule with its ledger tallies and staleness.
list--jsonMachine-readable, for /learning.
show<ID>One rule in full: its text, domain, state and ledger tally. What the TUI's Enter on the Rules pane runs.
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 that calls no model; it counts only the regressions measured on the model in use, so a night on another model neither adds to nor dilutes them. 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|supersede] [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.
supersede[ID]Retire a stale pending proposal, releasing its reflections unconsumed.
supersede--staleInstead of one id, sweep every pending proposal measured against a baseline that has since moved.
supersede--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.

Every pass that writes, with or without sessions to distill, also scores the appraisals' predictions and adds each decided comparison's losing arm to its session's appraisal. Neither makes a model call.

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 [FEATURE] [--json] [--write] [--minimal] [--undecline <STEP_ID>]
FlagDescription
FEATUREOnly that optional feature's step — mecha setup mail. A part sets up the feature it belongs to (ocr → documents); an id that is not a feature is refused by name. Naming a feature you declined, or switched off, offers it again for this run; nothing is written unless you answer. With --json nothing is offered, so it stays declined and exits 0.
--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.
--minimalDecline every optional step still outstanding, in one pass — the light install. Writes declines, never config; mecha features enable <id> turns any one on later.
--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.

At a terminal, mecha setup is one guided pass. It writes the starter config if there is none, then asks:

  1. The chat model, when nothing answers prompts yet: the model recommended for this machine (with its download), a GGUF you already have, a hosted model (Anthropic) instead, or skip. A server that answers but that the config does not describe is one question: write what it reports. Installing a model here needs Linux and :8080 free; elsewhere the menu offers what can be done.
  2. Each feature that is off, as yes, no or never, with what it does, what it would download, and what else a yes switches on. never is recorded at once, as in the step loop; mecha setup --undecline <id> undoes it.
  3. Once, with everything listed and one total: Start? Then it installs the chat model, switches on and installs the features, and runs any sign-in a feature still needs while you are there. Anything else a feature still needs — a binary to build, say — is named, not run.

What it does not cover — the charter, the scheduler, a feature switched on but not working — is offered after it, a step at a time. Naming a feature, a flag, or running without a terminal skips the guided pass. mecha setup chat and mecha features enable are still how to change one thing later.

--json, --write, --minimal and --undecline 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.

mecha setup engine​

mecha setup engine [--adopt [--force] | --upgrade [--to <TAG>] [--force] | --rollback [--now]]

engine is a word setup reserves, not a feature: on its own it says which llama.cpp each server runs — the router, the embeddings server and the OCR server — mecha's own builds, and the last measurement.

--adopt moves a machine whose servers run a llama.cpp you installed yourself onto mecha's pinned one, only if it measures no slower. It installs the pinned release beside yours, stops the router, and runs each server's own launcher on a private port — once on the engine it runs today, once on the new one — loading the model you have loaded, measuring generation and prefill (median of three), and checking a chat turn, an embedding (which must match the old engine's) and an OCR page, each where its server exists. The new engine wins when every check the old one passes, it passes too, and neither rate drops by more than the runs' own noise (at least 3 %). Then each unit gets a drop-in pointing it at mecha's engine, the on-demand servers are stopped so their next request starts on it, and the router restarts and is asked which engine it runs. Otherwise the router restarts on the old engine and nothing changes. Either way the result is a row in ~/.mecha/sidecars/llama/ledger.jsonl. --force moves the servers whatever the measurement says — though not onto an engine that could not be measured at all.

The router does not answer while it is measured — a few minutes — so --adopt asks first, and declines without measuring if any run holds the router or another switch is waiting. Your own llama.cpp is left where it is. Ctrl-C during the download or the measurement stops what it started and puts the router back on its engine with the model you had; once the servers are being moved it finishes that first. A machine where only some units run mecha's engine is refused: --rollback first, so every unit is measured against what it ran.

--upgrade moves an adopted machine to a newer llama.cpp. It finds the newest b release that publishes a build for this machine — or the one you name with --to b11399 — prints its tag, date and commit, and asks you to confirm that tag: no reviewer has pinned a release newer than the one mecha ships, so each archive is checked against the digest GitHub publishes for it, and your yes to the tag is what lets that stand in for a review. It installs the release beside the current one and measures both exactly as --adopt does; only if the new build is no slower does current move to it, the old build staying as previous. The router is then asked which build serves your model. A machine still on a llama.cpp you installed is sent to --adopt first.

--rollback steps back one change. After an upgrade, current returns to the previous build; after an adopt with nothing since, the drop-ins are removed, so the units run your llama.cpp again. Either way the router restarts on it and the rollback is a row in the ledger. It waits for running work, as any model switch does; --now asks the work to stop instead. mecha keeps two builds on disk — the current one and the way back — and removes any other.

All three refuse without a terminal: they change what every server runs.

mecha setup chat​

mecha setup chat

chat is the other word setup reserves: it is how a machine with no model server gets one. It shows the chat model recommended for this machine's memory tier — its download size and the context and slots it will be served with — or takes a GGUF you already have (and its vision projector, if any; ~/ is expanded). On a yes it installs mecha's pinned llama.cpp first unless mecha's own is already installed — a llama-server already on your PATH, Homebrew's say, is left alone and not run — then the router on :8080: a systemd user service on Linux, a launchd agent on a Mac. Its launcher is in ~/.mecha/sidecars/bin/, and its presets in ~/.mecha/sidecars/router/models.ini. It starts the router, waits for the model to load — several minutes for a large one — and asks it which model it serves. A model you brought stays yours, and running setup chat again is how to change it, or to fetch the model again if its files went missing; mecha features enable never installs the chat model.

The starter config's [providers.local] already names this router, so on a fresh install it shows what the router reports — model name, context window and vision — and asks before writing them into that table. When no local provider is configured at all, it offers to write one, from what the router reports about the model it installed — the same write-and-confirm step as mecha setup --write, so the model name, context window and vision are read off the server, never typed. When one already names this router — setup chat run again to change the model — it shows what the router now reports for that table and asks before writing, as mecha setup --write does, since the model name is what the router routes by. If that table is not your default_provider, it then offers to make it the default — once the table names what the router serves, whether it already did or you just let it be rewritten — so a local model is never installed while every run still goes elsewhere. A local provider that names a server on another machine, or on another port here, is left as it is.

A router you installed yourself, or one that could not be checked, is left alone: nothing is installed over it. A reinstall never cuts off a run: if one is using the router, or a model switch is pending, it declines before downloading anything, and it holds the router's switch while it restarts, so a run that starts meanwhile waits rather than failing. It takes no flags and runs only at a terminal, because it asks. On Linux only, as the other servers' installs are.

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 step offers two routes, local first, because a local model is what mecha is for: mecha setup chat installs one on Linux and points mecha at it. The other route 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 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 has a step for every optional feature — the web app, Slack, mail, docs, the knowledge graph, web search, PDF extraction, image generation, personas, voice, the front door and messages — read from the same registry as features, in that order, so a feature's dependency is offered before it:

mecha features saysThe step
ondone, with its parts (documents (ocr on, layout on))
switched off (false)declined — an answer already given; mecha features enable is the way back
not switched onoutstanding and declinable; the command is mecha features enable …, a dependency first
switched on, but waiting on another feature or not readywrong, not declinable — you said yes and it does not work; the next command or the edit it needs, and mecha features disable turns it off instead
unknownunknown, offering nothing

Setup only ever runs a command; an edit to config.toml (an [[mcp]] entry, a [[search]] backend, a [documents] table) is named in the step and left to you. The one table written for you is [image]: mecha features enable image writes it on its defaults when there is none. After a command works, it reads the feature again and says where it stands and what is next. Mail, docs and the graph count as set up only with an enabled [[mcp]] entry naming their server, as their tools do — an installed binary alone is not mail. 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 step runs nothing: it names mecha charter edit and Settings → Charter in the web app, and asks only later, or never? — writing priorities is a sitting of its own, and nothing here composes one.

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 optional features (while not switched on) 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.

system​

The state of the machine mecha runs on, read in one place. No model runs here.

mecha system probe [NAME | GROUP] [--json]
mecha system admit --need SIZE [--json]
mecha system sample [--json]
  • probe reads named measurements: one (memory.available), a group (gpu, pressure.memory), or all of them when none is named. A rate (disk.read_rate, cpu.busy) comes from the newest recorded minute. A reading that cannot be had says so, and why; it is never shown as zero. Work in flight is read the same way: runs.live, holds.live, jobs.live, triggers.missed and the rest are counts, never names, and voice.live says whether a call is live in any chat.
  • admit asks the memory floor whether something needing SIZE (24G, 512M, or a number of MB) may start now: the need plus [system] memory_reserve_mb must be available, and memory pressure must be under its ceiling. It exits non-zero when refused, so a build can chain on it: mecha system admit --need 24G && cargo build --release.
  • sample records one minute of the machine into ~/.mecha/system/series.sqlite: numbers and kinds of work only, never a process, unit or model name. The mecha-system-sample.timer runs it every minute.

features​

Which optional parts of mecha are on, the way to turn on each of the rest, and the switches themselves. Read from the global config and the disk only — no network, so a server that starts on demand is never woken to be asked.

mecha features [--json]
mecha features --probe [--json]
mecha features plan <id> [--verify] [--json]
mecha features enable <id>... [--no-install] [--turbo]
mecha features disable <id>...
FlagDescription
--jsonMachine output: one object per feature, with state one of on, off, blocked, unready, unknown, its switch (on, off, absent, or null for a part), in_use, shown (whether the web app shows it) and next (the command or config change that brings it on, or null).
--probeAdd up the memory of every model the shown features need — any not off or blocked, so one switched on but not yet set up counts too — against this machine: one pool on unified memory (a GB10, a Mac), or the card and system memory separately with a separate GPU. Each model is listed with what it holds and on what evidence — measured (machine and date), arithmetic, or unmeasured; a sum with an unmeasured figure in it is unknown, never zero, and what the figures leave out is named beneath. Reads /proc/meminfo (or sysctl on macOS) and runs nvidia-smi; asks no server, so nothing on demand is woken. With --json, the same as one object. The figures are the hardware page's.
FeatureRequirement and next step
mail_rulesRequires mail; mecha features enable mail mail_rules. Install the sibling mail binary and five-minute timer. Defaults off.

Every optional feature has a switch in the global config.toml's [features] table (see Configuration), and the switch comes first: a feature whose switch is absent or false reads off, whatever its settings say. With the switch on, the row reads its settings. on and off are what they say; an off row carries the command or config change that turns it on. blocked means something it needs is off — layout needs ocr, dictation in the browser needs the web app — and names the dependency, with the command that turns it on. unready means it is switched on but cannot work yet — its settings are missing, such as image = true with no [image] table, or a fact on disk says so, such as mail with no mailbox authorised. unknown means a store could not be read, which is a finding, never the same as off. Parts of a feature (tasks under graph, ocr under documents) are indented under it and have no switch of their own.

A feature whose switch is unanswered but which this install has set up — a mail server in [[mcp]], an [image] table, a persona in the store — is marked not enabled, but set up here. An install from before [features] existed has every switch unanswered; the same list is printed as one line when a session or a service starts (run, chat, tui, serve, voice-serve, slack connect, trigger daemon), and mecha setup offers each as a step. A feature that is set up but not yet usable is named with the reason; one whose store could not be read is never named.

plan says what a feature runs beside mecha and whether this machine already has it, then lists every model file it loads. A part plans as its parent, so plan ocr is plan documents.

  • Programs: each one is either provided — a systemd user unit, a binary on PATH, a directory or a Docker image shows it, and it is left alone — or not here, with the build step whose installer brings it.
  • Models: each pinned file is either in the cache and matching its pin (it costs nothing), a download at its pinned size, or kept by a provided program outside the cache (ComfyUI's own models). A file placed by hand at its path is reported as unverified unless --verify hashes it.

It installs nothing and asks no server: an idle server whose unit is there reads provided, never missing.

enable installs what the feature needs and this machine lacks, when mecha can install it. It shows the plan first, installs on one yes, and writes the switch only after every install has passed its check. What it can install:

  • Layout's environment (documents) — a pinned uv, a Python 3.12 it builds, the hash-locked packages and the pinned model, all under ~/.mecha/sidecars/layout/.
  • The llama.cpp engine — the official release for this machine (CUDA by the NVIDIA driver's version, Metal on Apple silicon, else the CPU build), checked by sha256, under ~/.mecha/sidecars/llama/<tag>/ with a current link. Where no release build fits — an NVIDIA driver older than the release's CUDA needs, or a Linux architecture no release covers — the same pinned version is built on the machine from its commit, which needs git, cmake, a C++ compiler and, for CUDA, the toolkit's nvcc; what is missing is named before anything is asked. The engine is offered only where the feature runs something on it — an embeddings or OCR server. A machine whose NVIDIA device nvidia-smi cannot read gets no engine rather than the CPU build.
  • The embeddings and OCR servers (embeddings for graph, documents and personas; OCR for documents) — the pinned model, a launcher under ~/.mecha/sidecars/bin/, and the three systemd user units that start the server on its first request and stop it after ten idle minutes; the install starts it once to check it answers.
  • ComfyUI (image) — a pinned commit with a hash-locked torch for CUDA 13, the image slot's pinned models and mecha-comfyui.service on :8188, under ~/.mecha/sidecars/comfyui/, plus a one-minute idle timer that hands a loaded model's memory back. It needs an NVIDIA driver 580 or newer on aarch64 or x86_64; elsewhere it is not offered and images switch on for a ComfyUI you run yourself. The check asks the server for every node and model a picture uses and draws nothing. enable image also writes an [image] table on its defaults when there is none.
  • Speech to text (voice) — Parakeet on sherpa-onnx from a hash-locked environment and its pinned model, mecha-parakeet.service on :8992, under ~/.mecha/sidecars/stt/.
  • The speech server (voice) — Breeze TTS 2: the engine built here from a pinned commit, its published weights and a default voice fetched by sha256, and its adapter, as mecha-breeze-tts.service on :8886 and mecha-breeze-adapter.service on :8887, under ~/.mecha/sidecars/speech/. It needs an NVIDIA GPU and the CUDA toolkit; elsewhere voice switches on for a speech server you run yourself. The weights are research and non-commercial only, and the install says so before fetching them. The check is a spoken sentence.
  • The voice worker (voice) — pipecat from a hash-locked environment, mecha-voice-worker.service on :7860, under ~/.mecha/sidecars/voice-worker/, reachable only through mecha serve's relay.
  • Code mode's interpreter (code) — the monty worker from PyPI's pinned pydantic-monty-runtime wheel, checked by sha256 against the pin for this platform, under ~/.mecha/sidecars/monty/<version>/. Nothing on PATH stands in for it. Only in a mecha built with the cargo feature monty; a build without it reads code as not in this build, and enable code refuses, naming the feature. With code = true, every run that would register code first runs a program in the worker under the sandbox (bwrap on Linux, so [sandbox] kind = "bwrap"; macOS's own Seatbelt profile on a Mac), and refuses to start if that fails rather than running without it.

On macOS the servers install as launchd agents in ~/Library/LaunchAgents/ instead of systemd units — the chat router (mecha setup chat) and the embeddings and OCR servers, started on their first request — each logging to ~/Library/Logs/mecha/, capped at 20 MiB and one older file. The chat model is never enable's: its engine, model and router are mecha setup chat's, where the model is chosen, so switching on the web app does not stand behind a chat model's download. A file mecha did not write is never replaced.

enable mail, docs and graph also write the feature's [[mcp]] entry where none runs its program or carries its name — the program by its absolute path, from PATH or ~/.cargo/bin, results untrusted — and, where the config has no [outbox], one staging mail's sends and documents' writes. Every server of one command shares that one [outbox], as do the yeses of one mecha setup. An entry or [outbox] you wrote is never edited — enable names what it lacks — with one exception: an entry running a copy of mecha's own mail, docs or graph server from another build has its command pointed at the matching build, and nothing else in it changes. No [outbox] is written where a [[rule]] of yours names a call it would stage, and the rule is named. enable voice writes [voice] voices_dir for the installed speech server where no [voice] table exists.

Answering no writes nothing. --no-install writes the switch without installing. Without a terminal, an enable that would install refuses and names --no-install. A program that is already on the machine — or a server already answering where an install would put one — is left alone and used. --turbo, with image, also fetches the turbo model (6.8 GiB) into the image server mecha installs. Without it, the turbo model is fetched only once the chat's or a persona's picture settings choose it, and then enable image offers it, restarting a server already installed to link it. The turbo model never costs the switch: without a terminal that restart is skipped, and answering no to it alone still switches images on.

enable writes true for each id, disable writes false, in place: comments, the order of the file and any key a newer build added are kept, and the table is created when it is missing. enable refuses a part (ocr: switch on documents and set [documents] ocr; incognito: part of the web app, offered wherever the chat model runs on this machine; world: switched per persona by [life] in its persona.toml), and a feature whose dependency is not on, printing the command that switches both. disable says what it leaves blocked — disable web names voice's dictation, calls and cloning, which live in the web app. A service that is already running reads the change when it restarts. Beyond [image], enable writes no settings: the [[mcp]] entry mail, docs and the graph need, a [[search]] backend and a [documents] table stay yours, and until they are there the feature's row stays off, naming the one missing as its next step.

Exit 0 whatever the list finds: an install with features off is a light install, not a broken one. A global config.toml that does not parse is an error (exit 1), since no row could be read from it; a [features] key this build does not know is reported and ignored. Mail, docs, the graph and publishing count as on only when an enabled [[mcp]] entry in the global config has a command naming their binary; switched on, an entry that starts the server through a wrapper script reads unready. An installed binary alone puts no tool on the surface, and a project's mecha.toml never changes the answer. A row that has a tool reads on only where that tool would register: turned off in [tools], an image or OCR server off this machine, or a search backend with no key reads unready — or off, for a part such as the library — not on.

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.
--from-workspace <PATH>Only sessions rooted at this path or beneath it. Not the global --workspace, which names this run's path jail.
--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|null|floors> [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.
ruminate--from-workspace <PATH>Only diagnose from sessions rooted at this path or beneath it.
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.
reject--verdict <V>Why: harmful, no-effect or unmeasured. On a candidate already rejected, retypes it.
revert<ID|KEY>Take an accepted override back out.
overridesThe active override layer, with where each entry came from.
null--sessions, --days, -n, --holdout-in, --from-workspaceAs ruminate: the replay with nothing changed on either arm.
null--drawn-by <METRIC>The metric the selection is ranked by. Default tool_error_rate.
floors--jsonThe gate's floors, beside what the null runs measured.

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.

reject --verdict decides what the next nightly diagnosis is told about that change. harmful means never propose it again. no-effect means not until the measurement method changes. unmeasured means the measurement could not tell, so it may be proposed again. A rejection with no verdict is a plain no.

null runs the nightly replay with nothing changed. Any verdict it gets is the gate reacting to noise, and floors shows how often that has happened. Until a null run exists, the gate's thresholds are asserted rather than measured.

mecha harness ruminate --days 7
mecha harness list
mecha harness show hc-20260828T0330
mecha harness overrides
mecha harness revert max_turns
mecha harness reject hc-20260903T0345 --verdict unmeasured
mecha harness null --sessions 16
mecha harness floors

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>Proposer of the class to work. Default bee:suggested.
--predicate <P>Predicate of the class to work. Default related_to.
--limit <N>Candidates to judge, oldest first. Default 10.
--since <DATE>Evidence on/after this date — both readers get the same window, so a difference must be the sources disagreeing. Default 2024-01-01.
--min-coverage <N>Minimum episodes a source needs before it can be a vantage. Default 3.
--server <S>The [[mcp]] server holding the graph. Default graph.
--recordFile the verdicts beside their candidates. Off by default.
--out <PATH>Append one JSON line per judged candidate — verdict, both sightings with citations, and the dissenter's pre-reveal answer.

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. Default 3. 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. Default 2024-01-01.
--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. Default 8.
--adjudicate <N>Pending claims about this entity to adjudicate after the exchange; 0 skips it. Default 25.
--server <S>The [[mcp]] server holding the graph. Default graph.
--out <PATH>Append the whole run as one JSON line — exchange, graph findings, audit verdicts.

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.

model​

The local model router: what it can serve, and which model it holds. Loading a model is the choice — every run that takes a provider marked follow_loaded by default uses whichever model is loaded, with no restart and no setting to edit. list is the default subcommand.

mecha model [list|use|cancel-switch] [ARGS] [--json]
SubcommandFlagDescription
listEach router's models, which one is loaded (●), the provider entry naming each, and any entry whose temperature disagrees with its model's preset.
use<NAME>A provider entry or a router model name. Loads it and waits until it is resident.
use--nowDon't wait for the runs using the loaded model: ask each to stop at its next safe point, give them 15 s, then switch. A reply in progress ends early. Without it, the switch waits for every run to finish, with no time limit. For a switch already waiting on the same model, --now hurries that switch instead of being refused as a second one.
use--wait-secs <N>Give up on the load after this many seconds (default 600; a cold load measured 33–39 s). Separately bounds a loaded model that stays busy answering a request before the load: the router won't evict a busy model, so the switch asks again each second meanwhile, still cancellable and still hurried by --now. The wait for runs before either is not bounded.
cancel-switchWithdraw every pending switch, on every router — the way out of one whose mecha model use is gone or whose file can't be read, which every run on that router would otherwise wait for. A live switch is better stopped with Ctrl-C where it runs.
list, use--jsonMachine-readable output. An unreachable router is listed with "reachable": false. list also gives each router's pending_switch (its target, the runs it waits for, readable: false for a switch file nobody can read, which only cancel-switch clears, and past_the_wait once nothing is left to hurry or withdraw: the router has answered the load, or --now is about to unload the old model) and, per model, would_not_follow: why default runs would not follow it once it is loaded. The web chat's model chip reads this.

Behind a router, --model on any command selects as well as names: mecha run --model qwen3.8-27b "…" loads that model, evicting the one that was loaded, and every later default run then follows it — a one-off flag changes the pick. mecha model use is the deliberate way to do the same. Prefer naming the sibling entry (-p gemma26) to a bare --model <id>: the entry brings that model's context_window, temperature and prices, where --model keeps the default entry's and the startup check will warn about the mismatch.

use refuses a model whose preset temperature disagrees with its provider entry, because mecha sends temperature on every request and would silently re-tune it. If the new model fails to come up, the one it was replacing is loaded back.

mecha model
mecha model use gemma26
mecha model use qwen3.6-35b-a3b-uncensored --now

See Switching models for what follows a switch and why it waits, and follow_loaded in the configuration reference.