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).
| Flag | Description |
|---|---|
-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, --yes | Approve every tool call without asking. Required for unattended runs that need to write or execute. |
--read-only | Refuse 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-mcp | Skip MCP servers entirely. |
--no-mcp-server <NAME> | Skip these MCP servers by name. Repeatable; for turning one off while the rest stay. |
--no-thinking | Turn off reasoning. Cheaper and faster, but noticeably worse on multi-step work. |
--no-skills | Don'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-charter | Don't load ~/.mecha/charter.toml into the system prompt — the run proceeds with no standing priorities. |
--no-learned-rules | Do not inject learned rules from ~/.mecha/learning into the system prompt. |
--no-hooks | Do not run configured [[hook]] commands. Config is still validated. |
--no-outbox | Do not route any tools through the outbox; configured [outbox] tools execute directly. |
--no-messages | No inter-agent messaging: no message_send tool, and nothing from the mailbox is delivered into this run. |
--no-fallback | Never fall back to another provider. A transient failure that survives its retries fails the run. |
--no-compact-tool | Withhold the model's compact tool; automatic compaction remains available. |
--no-code-tool | Withhold code, code mode's program tool, even with [features] code on. mecha eval forces it. |
--no-step-escalation | Disable quarantined checks of ambiguous completed plan steps. |
--no-step-checks | Do not execute declared plan checks. |
--no-goal-guidance | Disable goal and charter planning guidance. |
--no-boredom | Disable notices about approaches that stop yielding new evidence. |
--no-compact-validate | Skip omission checks on compaction summaries. |
--no-predictive-compaction | Trigger on reported context size only; output budgeting and headroom forecasts remain active. |
--no-carried-state | Do not carry tool-owned plan state across compaction. |
--compact-at <N> | Summarise older turns once the prompt passes this many tokens. |
-v, --verbose | Print tool calls, results, and token usage as they happen. |
-h, --help | Print help. |
-V, --version | Print 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]
| Flag | Description |
|---|---|
[PROMPT] | The task. Omit it, or pass -, to read from stdin. |
--json | Emit a single JSON object instead of prose. Implies --quiet. |
--quiet | Print only the answer — no tool narration. |
--no-stream | Wait 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-session | Do 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]
| Flag | Description |
|---|---|
--resume <ID> | Continue a saved session by id or unique prefix. |
--no-session | Do 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]
| Flag | Description |
|---|---|
--resume <ID> | Continue a saved session by id or unique prefix. |
--no-session | Do 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:
| Command | Description |
|---|---|
/help | This list. |
/tools | Tools this agent can call. |
/skills | Procedures this agent can load, and which are loaded. |
/charter | Standing priorities, ranked — e edits them in $EDITOR. |
/triggers | Scheduled prompts: see, edit, run, cancel. |
/queues | Every store waiting on you, incl. the graph merge queue. |
/learning | Reflections, rules and proposals — read, edit, refuse. |
/outbox | Staged outbound drafts: read, edit, send, reject. |
/mail | The classified inbox: read, dismiss, make a task (alias /inbox). |
/frontdoor | Inbound requests: read, extract, triage, close. |
/tasks | The 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. |
/entity | Who is who in the graph: rename, alias, add a person. |
/polls | Open polls on the gate: tallies, close, export. |
/doctor | What is silently wrong across the stores, and the way out. |
/docs | Documents 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. |
/usage | Tokens used this session. |
/clear | Start a new conversation, dropping its taint. |
/session | Where the transcript is being written. |
/todo | Show 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. |
/exit | Quit (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.
| Flag | Description |
|---|---|
--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-yes | Allow 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>
| Flag | Description |
|---|---|
<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]
| Flag | Description |
|---|---|
[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. |
--failures | Show 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-workspaces | Keep the staged workspaces of sandboxed cases instead of deleting them. |
--mcp | Connect 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-user | Withhold ask_user, which is otherwise part of the tool surface. |
--ab-rules | Run 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
| Subcommand | Flag | Description |
|---|---|---|
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-run | Plan 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]
| Flag | Description |
|---|---|
--schema | Print the full JSON schema for each tool, exactly as the model sees it. |
--json | Emit 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]
| Flag | Description |
|---|---|
--show | Print each skill's full body, exactly as the model would receive it. |
--json | Emit 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]
| Subcommand | Flag | Description |
|---|---|---|
| (none) | --json | Emit 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. |
edit | Open 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]
| Subcommand | Flag | Description |
|---|---|---|
list | -n, --limit <N> | How many to show. Default 20. |
show | <ID> | Session id or unique prefix. |
show | --json | Emit 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 | --json | Emit 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 | --json | Emit 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 | --json | Emit 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-tests | Include smoke-test sessions (MECHA_SESSION_KIND=test); --kind test implies this. |
appraise | --probe | Resolve 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 | --appraise | Retired. 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, --json | As 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>
| Flag | Description |
|---|---|
<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. |
--json | Emit 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]
| Subcommand | Flag | Description |
|---|---|---|
list | List 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 | --json | Edit 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 | --all | Every 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, --yes | The 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]
| Subcommand | Flag | Description |
|---|---|---|
queues | What is waiting, across every store. An unreadable store shows —, never 0. | |
proposers | The 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. |
shadow | The 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 | --all | Group 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-run | Cap the bulk match (the graph defaults to 500), or see what would be hit without changing anything. |
accept | --create-subjects | A 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-classes | Cascade over an explicit comma-separated member list from a groups listing; --across-classes lets those ids span classes (pair with groups --all). |
reject | Same 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]
| Subcommand | Flag | Description |
|---|---|---|
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. |
list | Messages, pending first, across every mailbox. | |
list | --to <NAME> | Only this recipient's mailbox. |
list | --all | Include 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 | --all | Every pending message instead. |
dismiss | --to <NAME> | With --all: only this recipient's mailbox. |
agents | Which 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]
| Subcommand | Flag | Description |
|---|---|---|
list | What 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-run | Say 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>
| Subcommand | Flag | Description |
|---|---|---|
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 | --json | The whole extraction, including each page's text-layer regions with their boxes in PDF points. |
extract | --no-cache | Neither 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.
| Subcommand | Flag | Description |
|---|---|---|
list | --all | include threads already acted on, and the ones classified ignore |
list | --aged | day 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 | --surface | record that these were surfaced, so they are not surfaced again. Deliberately separate from reading the list |
list | --json | machine 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 | --force | re-classify threads already in the store |
classify | --dry-run | say 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 | --json | pass the tool's rows through |
compose | --to / --subject / --body / --cc | write 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> / --json | every account's calendars, with write access noted |
task | --name / --due / --context / --project | the 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 / --deadline | field-level; none clears a field |
reflect | --dry-run | turn 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.
| Subcommand | Flag | Description |
|---|---|---|
list | --closed | also show done and dropped — the history |
list | --json | machine 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 / --project | Omit 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 | --unattended | Run 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 | --again | Start 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
| Commands | Purpose |
|---|---|
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, close | Define 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, ack | Configure 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]
| Subcommand | Flag | Description |
|---|---|---|
notes | --limit <N>, --json | Recent notes, newest first. Default 20. |
search | <QUERY>..., -k <K>, --json | The same context pack the model gets; #tag tokens filter to hand-tagged episodes. Default 10 results. |
entity | <NAME>..., --json | Everything 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>, --json | The bounded neighborhood over current facts. Default 1 hop. |
timeline | <NAME>..., --json | Superseded 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]
| Subcommand | Flag | Description |
|---|---|---|
list | What 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. |
extract | Run the quarantined extraction over everything not yet extracted. | |
extract | --seq <SEQ> | Just this one. |
extract | --force | Re-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. |
triage | Draft 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]
| Subcommand | Flag | Description |
|---|---|---|
status | What is bound, and whether the credential still works. | |
auth | Store 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 | --force | Bind 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. |
connect | Run the connector: hold the Slack socket open and drive runs from threads. | |
sweep | Mark 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 | --sweep | Named threads a TUI session is mirrored into. --sweep cools any whose session has gone. |
unlink | Forget 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]
| Subcommand | Flag | Description |
|---|---|---|
list | Triggers, 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 | --disabled | Create it switched off. |
add | --force | Overwrite an existing trigger of the same name. |
show | <NAME> | The trigger's settings and its recent runs. |
show | --last | Print 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-run | Say what would fire, and fire nothing. |
daemon | --print-unit | Tick 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]
| Flag | Description |
|---|---|
--sessions-dir <DIR> | Directory of session transcripts. Defaults to the standard location. |
--dry-run | List what would be mined without calling a model or writing anything. |
--limit <N> | Mine at most this many sessions this run. |
--remine-untrusted | One-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-situations | One-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]
| Subcommand | Flag | Description |
|---|---|---|
list | --domain <D> | Only behavior, writing or triage. |
list | --all | Include dropped ones, hidden by default. |
list | --json | Machine-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]
| Flag | Description |
|---|---|
--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. |
--propose | Measure the candidate and stage a surviving proposal for review. |
--auto | Measure and apply unless a probe regresses; ungradeable candidates apply on probation. Conflicts with --propose. |
--dry-run | Show what would run without calling a model or writing anything. |
--compare-sources | Learn 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. |
--json | With --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]
| Flag | Description |
|---|---|
--unprocessed-only | Only 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-attribute | Skip the bisection that attributes a regression to one rule. Regressions are still recorded, just unattributed. |
--repeat | Re-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]
| Subcommand | Flag | Description |
|---|---|---|
list | Every rule with its ledger tallies and staleness. | |
list | --json | Machine-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 | --apply | Apply 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]
| Subcommand | Flag | Description |
|---|---|---|
list | List proposals. | |
show | <ID> | The rules diff and the gate's evidence. |
accept | <ID> | Apply a pending proposal to the live rules. |
accept | --force | Apply 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 | --stale | Instead 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]
| Flag | Description |
|---|---|
--sessions-dir <DIR> | Directory of session transcripts. Defaults to the standard location. |
--server <SERVER> | The [[mcp]] server holding the knowledge graph. Default graph. |
--dry-run | List 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>]
| Flag | Description |
|---|---|
FEATURE | Only 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. |
--json | Print the plan as JSON and exit. Never prompts, even at a terminal. |
--write | Rewrite the local provider's model, context_window and vision from what its server reports. |
--minimal | Decline 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:
- 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
:8080free; elsewhere the menu offers what can be done. - 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.neveris recorded at once, as in the step loop;mecha setup --undecline <id>undoes it. - 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:
| Setting | Wrong how |
|---|---|
context_window | naming -c rather than the per-slot -c / -np, so a run compacts at a threshold nobody chose |
vision | unset against a multimodal server, so every image arrives as a line of text |
model | naming 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 says | The step |
|---|---|
| on | done, 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 on | outstanding and declinable; the command is mecha features enable …, a dependency first |
| switched on, but waiting on another feature or not ready | wrong, 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 |
| unknown | unknown, 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
wrongstays wrong (declining mail is not declining to be told your mail is broken), somethingunknownstays 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]
| Flag | Description |
|---|---|
--json | Machine 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]
probereads 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.missedand the rest are counts, never names, andvoice.livesays whether a call is live in any chat.admitasks the memory floor whether something needingSIZE(24G,512M, or a number of MB) may start now: the need plus[system] memory_reserve_mbmust 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.samplerecords one minute of the machine into~/.mecha/system/series.sqlite: numbers and kinds of work only, never a process, unit or model name. Themecha-system-sample.timerruns 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>...
| Flag | Description |
|---|---|
--json | Machine 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). |
--probe | Add 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. |
| Feature | Requirement and next step |
|---|---|
mail_rules | Requires 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
--verifyhashes 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 pinneduv, 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 acurrentlink. 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'snvcc; 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 devicenvidia-smicannot read gets no engine rather than the CPU build. - The embeddings and OCR servers (embeddings for
graph,documentsandpersonas; OCR fordocuments) — 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 andmecha-comfyui.serviceon: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 imagealso 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.serviceon: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, asmecha-breeze-tts.serviceon:8886andmecha-breeze-adapter.serviceon: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.serviceon:7860, under~/.mecha/sidecars/voice-worker/, reachable only throughmecha serve's relay. - Code mode's interpreter (
code) — themontyworker from PyPI's pinnedpydantic-monty-runtimewheel, checked by sha256 against the pin for this platform, under~/.mecha/sidecars/monty/<version>/. Nothing onPATHstands in for it. Only in a mecha built with the cargo featuremonty; a build without it readscodeas not in this build, andenable coderefuses, naming the feature. Withcode = true, every run that would registercodefirst 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]
| Flag | Description |
|---|---|
--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-run | Print 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]
| Subcommand | Flag | Description |
|---|---|---|
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 | --all | The whole record, not just what is waiting on you. |
list | --json | Machine-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. |
overrides | The active override layer, with where each entry came from. | |
null | --sessions, --days, -n, --holdout-in, --from-workspace | As 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 | --json | The 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]
| Flag | Description |
|---|---|
--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. |
--record | File 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]
| Flag | Description |
|---|---|
--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. |
--record | File 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]
| Flag | Description |
|---|---|
--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]
| Subcommand | Flag | Description |
|---|---|---|
show | Print the merged configuration as TOML. | |
path | Print the files that are being read, and whether they exist. | |
init | --project | Write ./mecha.toml instead of ~/.mecha/config.toml. |
init | --force | Overwrite 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]
| Subcommand | Flag | Description |
|---|---|---|
list | Each 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 | --now | Don'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-switch | Withdraw 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 | --json | Machine-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.