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-step-escalation | Disable quarantined checks of ambiguous completed plan steps. |
--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> | Explicitly confirm this run's goal. |
--appraisal-evidence <FILE> | Owner-authored commitment/check evidence for the matching --goal; see anticipatory appraisal. |
--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 | The list. |
/tools | Tools this agent can call. |
/triggers | Scheduled prompts: see, edit, run, cancel. |
/outbox | Staged sends and publishes: show, edit, send, reject. |
/queues | Every store waiting on you — incl. the graph's merge queue, reviewed in place. |
/frontdoor | Inbound requests: extract, triage, close. |
/polls | Open polls, their tallies, and the lecture controls. |
/review [now|later|auto] | What happens to drafts a run stages. |
/model [id] | Show or switch the model. |
/provider [name] | Show or switch the provider. |
/mode [ask|allow|read-only] | Show or switch the permission mode. |
/mcp [on|off] | List MCP servers, or turn them all off and on. |
/mcp <server> [on|off] | Turn one server off and on. |
/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. |
/exit | Quit. |
The status line shows context use as a fraction of the window when
[providers.X] context_window is configured. Steering is a property of this
front-end: it needs one owner of stdin, which a readline REPL cannot be while a run
is streaming.
mecha tui -w ~/code/my-project
serve
Serve the web app on loopback behind Tailscale. Configure
[web] owner_login in the global config; assets points to a separately built
copy of web/dist.
| 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> | Mounted voice facade; default 8990, 0 disables. |
--voice-yes | Allow voice calls without per-call approval; configured outbound actions still stage. |
--offer-target <URL> | Voice worker offer endpoint; default http://127.0.0.1:7860/api/offer. Empty disables proxying. |
mecha serve --assets ./web/dist
The browser app sends X-Mecha-Request: 1 on mutations. Scripted API clients
must send it too and explicitly open a chat with POST /api/chat/{key} before
reading its transcript or events. See The web surface.
voice-serve
Standalone loopback facade for the local voice worker. The normal browser setup
mounts this inside mecha serve; use a separate process when that separation is
needed. The transcription and speech stack is installed separately.
mecha voice-serve --port 8990
--port defaults to 8990; --token optionally requires a bearer token.
See Voice for the worker and service setup.
batch
Run the same agent over a JSONL file of prompts, with bounded concurrency.
mecha batch [OPTIONS] <INPUT>
| 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. Default 3. |
--compare <FILES>... | Compare previously written scorecards side by side instead of running. |
mecha eval exits non-zero when anything fails, so it works as a regression gate.
It forces MCP off, hooks off, learned rules off, the outbox off and fallback off, so
a scorecard grades the model it names rather than this machine's local setup.
--runs k matters more than it looks: reliability decays much faster than mean
success, and the gap between pass^k and pass@k is the model's unreliability. A
pinned seed at --concurrency 1 replays token-for-token, making the k samples one
sample counted k times; the harness warns when it detects that.
--mcp-file resolves relative paths in a server's command/args against the
file's own directory, and a server that fails to connect is fatal here.
--ab-config overrides a closed set of run options — compact_at_tokens,
max_turns, max_output_tokens, effort — so both arms are built by one code
path. Unknown keys are refused, and every override is parsed before the first arm
runs. Neither arm is filed as an ordinary scorecard, and it always exits 0: a
delta is a finding, not a gate.
mecha eval -p local -m qwen3-moe -o results/qwen.json
mecha eval -p anthropic -o results/opus5.json
mecha eval --compare results/*.json
mecha eval --tag chaining --failures # one slice, with reasons
mecha eval -k 5 -o results/qwen-k5.json # pass^5 beside pass@5
mecha eval eval/graph-cases.jsonl --mcp-file eval/mcp.toml
mecha eval --ab-config max_turns=40 # measure a proposed change
exp
Run a designed comparison with named arms and isolated homes. The manifest fixes
the tasks and treatments before trials run; this is separate from mecha eval.
mecha exp new eval/assistant-lifetime.toml
mecha exp run assistant-follow-through --dry-run
mecha exp run assistant-follow-through --limit 3
mecha exp status assistant-follow-through
mecha exp judge assistant-follow-through --json
mecha exp export assistant-follow-through
new refuses an existing name. run resumes unfinished trials and skips finished
ones. See Experiments for manifests, fixtures, and
how to interpret the gate.
tools
List the tools an agent would see. Runs without any provider configured, which makes it a good MCP-server smoke test.
mecha tools [OPTIONS]
| 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.
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. |
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 appraisal page),
is printed beside the line as its kind and setpoint — the one thing about a
charter a run's prompt does not carry — and rides in --json as
sensor: {kind, setpoint} on the line, absent when there is none.
Editing through this rather than a hand-run vi buys validation feedback: a
duplicate id, a typo'd table name, an unknown sensor kind or a setpoint in the
wrong unit is reported the moment the editor closes,
and edit exits non-zero if what you saved will not load — rather than at the
next run's startup, where the warning scrolls past. Reading the file back is the
ordinary loader, so what it says is what a run would get.
A charter that fails to parse exits non-zero rather than degrading to none, because every run would otherwise proceed silently un-chartered. Output distinguishes three states a naive reader would conflate: no file at all, a file with no lines yet, and a file that did not load.
mecha charter
mecha charter edit
mecha charter --json | jq -r '.lines[] | "\(.id)\t\(.text)"'
mecha charter --json | jq '.over_budget'
See Goals and appraisal.
sessions
Inspect saved transcripts. Requires a subcommand.
mecha sessions <list|show|path|stats|health|appraise> [OPTIONS]
| 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. |
appraise, health | --kind <KIND> | Filter to a recorded surface, such as web, task, or tui. |
appraise, health | --include-tests | Include smoke-test sessions; --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 | --appraise | Run the quarantined appraiser over each session's numeric evidence. Paid, and independent of --probe. |
appraise | --max-appraisals <N> | Ceiling on appraisals driven. Default 25. Requires --appraise. |
stats totals token usage — and cost, where prices are configured — grouped by
provider and model. Transcripts live in ~/.mecha/sessions unless
MECHA_SESSION_DIR says otherwise.
health is the other question: not what runs cost but how they went — stop
causes, tool calls against errors and denials, runs that finished over a failed
call, compactions taken. Rates split by model, because a blend across two
describes neither, and a rate with no denominator prints — rather than 0%.
Transcripts written before the outcome record carry none, so the corpus fills as
you use it. health also reports goal drift
and null/reopened steps,
with per-run rates and explicit denominators. See Run quality.
appraise is the third question: not what runs cost, nor how they went, but how
they went against what they were for — the signed error per channel and the
label derived from it. Nothing is stored; each appraisal is derived on the spot
from transcripts, outcomes, drafts, questions, front-door records, clean
correction reflections, and the charter. Read the separate positive/negative
valence alongside the label counts, goal-confirmation coverage, and source
readability flags. Both paid passes
are off by default and are counted apart from each other in --json, where
absent means did not run rather than found nothing.
--probe builds a real agent with a real workspace jail, so run it from a
project directory or name one with --workspace; from a home directory it
refuses, because the jail would cover ~/.mecha. See
Goals and appraisal.
mecha sessions list -n 50
mecha sessions show 20260805T091500 --json | jq -r 'select(.role == "user") | .content'
mecha sessions stats --days 30
mecha sessions health --days 30
mecha sessions health --json | jq '.by_model'
mecha sessions appraise --days 30
mecha sessions appraise --days 7 --probe --max-probes 10
mecha sessions appraise --json | jq '.labels'
cat "$(mecha sessions path 20260805T091500)"
replay
Re-run a recorded session against its recorded tool results and report where the model diverged.
mecha replay [OPTIONS] <SESSION>
| 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 abandons the recording
and keeps going against the real tools.
mecha replay 20260805T091500
mecha replay 20260805T091500 --on-divergence error --json > replay.json # CI gate
mecha replay ~/.mecha/sessions/20260805T091500-3f2a1b7c.jsonl -m qwen3-14b -p local
outbox
Review, edit, release, or reject staged outbound actions. list is the default
subcommand.
mecha outbox [list|show|edit|review|approve|reconcile|reject|anticipate|outcome] [ARGS]
| 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. |
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. |
approve | <ID> | Execute the item's tool call, for real, and mark it sent. |
approve | -y, --yes | Skip the confirmation shown for items drafted in a tainted conversation. |
reconcile | <ID> --outcome delivered|not-delivered --evidence <TEXT> | Record an uncertain delivery's observed outcome; never sends. |
reject | <ID> | Refuse an item. It stays on file as the record of the refusal. |
reject | --reason <REASON> | Why — recorded on the item for the next reader. |
anticipate | <ID> [--file <FILE>] [--guide|--observe] | Read predictions or attach owner evidence to a pending message. --guide requires a current, resolved assessment before release; omitting the mode preserves it. |
outcome | <ID> --file <FILE> | Record post-delivery owner feedback linked to the prediction; revisions explicitly supersede prior feedback. |
approve is the current verb; send remains an alias. The original draft is
kept after editing, and mecha reflect mines the edit into writing lessons.
Delivery attempts are recorded durably. An uncertain outcome blocks retries
until the owner checks the destination and reconciles it; see
delivery recovery.
An item's kind decides how it is reviewed, not how it was staged. A
publish shows the rendered page rather than the arguments, and refuses
edit — see Publishing.
mecha outbox
mecha outbox show 3f2a
mecha outbox edit 3f2a && mecha outbox send 3f2a
mecha outbox review --all --kind message
mecha outbox reject 3f2a --reason "wrong recipient"
review
Everything waiting on a human, across every store — and the knowledge graph's
merge queue, decidable from mecha. queues is the default subcommand. See
The queues for the design.
mecha review [queues|proposers|list|sample|items|accept|reject] [ARGS]
| 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 classes, optionally one mechanism's. |
sample | --proposer <P> --predicate <PRED> | Individual candidates drawn uniformly at random — the default way to look at items, because judging the head of an ordered queue measures the ordering. |
sample | -n <N>, --seed <S> | Sample size (default 12) and the seed. Omit the seed and one is drawn and printed, so any sample can be redrawn and checked. |
items | --proposer <P> --predicate <PRED> | Queue order, for a class already decided about. Says outright its verdicts are not a rate. |
accept | [IDS]... | Accept candidates by id. |
accept | --proposer <P> --predicate <PRED> | A whole class. A cluster kind like (commitment) is refused by name rather than matching nothing. |
accept | --limit <N>, --dry-run | Cap the bulk match (the graph defaults to 500), or see what would be hit without changing anything. |
reject | Same shapes as accept, plus --reason. |
Every subcommand takes --json. The graph verbs drive the mecha-graph
binary as a child process ($MECHA_GRAPH_BIN, else PATH) — deliberately
not an MCP tool, so nothing a model can call accepts a fact candidate.
mecha review
mecha review proposers
mecha review sample --proposer bee:suggested --predicate related_to -n 12
mecha review accept --proposer linker:knn --predicate related_to --dry-run
msg
Messages between this machine's own agents — a chat session, a trigger, a
one-shot run — addressed by producer name rather than by session, so an
overnight trigger can write to chat without knowing which chat will read it.
Delivery happens at the recipient's next turn boundary. Requires a subcommand.
mecha msg <send|list|show|dismiss|agents> [ARGS]
| 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. |
The agents are wired up by [messages] enabled, which is off by default, but
this surface is not gated on it: the store is yours, and "what did the overnight
run tell me" must not depend on a feature flag.
dismiss rather than rm is the shape that matters — a full mailbox refuses
new sends, so a backlog nobody is coming to claim needs setting aside, and the
message stays on file either way.
A send from a terminal is stamped clean, because a person typing is the one
sender whose words are trusted input. A send whose stdin is not a terminal — a
pipe, a script, or an agent's shell reaching for mecha msg send to route
around the harness — is stamped private and untrusted, so the receiver's
interlock sees it exactly as message_send would have presented it.
mecha msg send chat "the briefing is in ~/.mecha/work/briefing"
mecha msg list --all
mecha msg show 9c1e
mecha msg dismiss --all --to chat
mecha msg agents
work
What runs have generated, and removing what is past. Every producer — a
trigger, a chat, a session — writes into its own directory, which is also the
path jail its runs get. list is the default subcommand.
mecha work [list|path|clean] [ARGS]
| 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). |
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.
mail
The inbox as a queue you work. list is the default subcommand.
mecha mail [list|show|classify|reply|forward|schedule|archive|spam|task|needs-info|correct|dismiss|reflect|score|eval] [ARGS]
Threads are named by an eight-character handle — the last eight characters of the id — and any unique suffix is accepted. A suffix rather than a prefix because Outlook conversation ids share a 57-character common prefix. Ambiguity is an error, never a guess.
| 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 |
reply | --note <TEXT> | extra steering — "decline politely", "ask for the deadline first" |
forward | --to <ADDRS> | comma-separated recipients |
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 |
eval | --sample / --seed / --prefilter-only / --out <PATH> | grade the classifier against a corpus whose outcome is known |
Every subcommand takes --account <NAME>.
reply, forward and schedule stage into the outbox
and never send. archive and spam reach nobody outside your own mailbox and
so are not staged. Separate verbs rather than one --action argument, because a
free-form label would put spam inside a verb that reads as harmless.
eval writes nothing to the triage store: grading year-old mail is not triaging
it, and a scorecard that mutated the queue it measures would be unrepeatable.
score reads the corpus written by mecha-mail corpus, not the MCP tools — a
measurement keyed on a display format breaks silently the day the format
changes.
mecha mail classify --account dartmouth
mecha mail list
mecha mail list --aged --surface # what the morning briefing runs
mecha mail show 3f2a1b7c
mecha mail reply 3f2a1b7c --note "decline politely"
mecha mail correct 3f2a1b7c --bucket respond --urgency today
mecha mail task 3f2a1b7c --due +3d
mecha mail reflect --dry-run
mecha-mail corpus --since 2026-07-01 --account dartmouth && mecha mail score
See Mail and calendar.
tasks
The GTD board in the knowledge graph. list is the default subcommand.
mecha tasks [list|add|set|source|work|stop|steer] [ARGS]
Reached through the MCP tool surface (kg_task_list / kg_task_create /
kg_task_update), the same way the model reaches it — so this is one reader of
one store rather than a second copy of it, and a configuration with no graph
server says so instead of showing an empty board.
| 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. |
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
Goals and appraisal.
mecha tasks
mecha tasks add --due +3d --context @lab -- Re-run the eval set on the new prefix
mecha tasks set task-1a2b3c4d --status next
mecha tasks set task-1a2b3c4d --due "" # clear it
mecha tasks list --closed
Delegation leaves the task yours to close. Its workflow tracks questions, drafts, and completion evidence across continuations.
The /tasks modal in mecha tui drives these operations, and
mecha-graph tui screen 6 is the same board with the same status letters.
workflow
Track an outcome across tasks, conversations, drafts, and owner decisions. Delegating a board task creates its workflow automatically. Workflows supplement the graph task board; closing one does not close the graph task.
mecha workflow today
mecha workflow add "Prepare the grant reply" --workspace ./grant
mecha workflow show FLOW_ID
mecha workflow check FLOW_ID --artifact reply.md --contains "revised budget"
mecha workflow verify FLOW_ID
mecha workflow close FLOW_ID
| 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 or explicitly reopen; cancellation does not stop an active runner. |
recover <ID> --reason <TEXT> | Clear stale ownership after confirming the old runner stopped. |
attention, tick, snooze, 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
mecha kg search "grant planning" --k 5
mecha kg entity "Priya" --json
mecha kg note "Discussed the grant timeline with Priya."
note --edit <SOURCE_ID> rewrites an existing note; it does not retract claims
already extracted from the old text. assert and retract maintain facts;
alias and unalias maintain names using node IDs. related reads a bounded
neighborhood; timeline includes superseded facts. See
The graph and each subcommand's --help.
frontdoor
Requests that arrived through the public surface, and the quarantine they pass
through before any run with tools is told about them. list is the default
subcommand.
mecha frontdoor [list|show|extract|next|triage|needs-info|close] [ARGS]
| 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. |
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. |
learn rewrites rules/<domain>.learned.toml within a fixed character budget;
rules/<domain>.user.toml is yours and is never written by code. The store is a git
repo, so git log is the learning history and git revert is the undo. Non-clean
reflections are excluded structurally, before any prompt is built.
--holdout is deterministic (every k-th by id), because a measurement set that
changes between runs measures nothing. --propose gates the candidate by
counterfactual replay first and stages what survives for mecha proposals.
mecha learn --dry-run
mecha learn --holdout 0.25 # leave a measurement set for validate
mecha learn --auto --holdout 0.25 -p local # the supplied automation
mecha learn --propose -p local # require owner review
learning-report
Read correction trends, rule health, and consolidation history without a model call or network access.
mecha learning-report
mecha learning-report --json
--bucket-days <N> controls the trend bucket width (default 7).
Use it alongside mecha rules to distinguish rules that improved measured work
from rules that have not yet been measured. See Learning.
validate
Probe whether the learned rules change the answers at the recorded moments the user stepped in, and append every outcome to the validation ledger.
mecha validate [OPTIONS]
| 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). Default is all three. |
--judge-model <MODEL> | Judge model id. |
--judge-provider <PROVIDER> | Provider entry the judge runs on. Defaults to the model under test. |
--no-attribute | Skip the bisection that attributes a regression to one rule. Regressions are still recorded, just unattributed. |
Steer and denial probes are counterfactual replays: the recorded prefix is driven
again, with and without the rules, and the verdict is structural. Followup probes are
judge-graded, so n=1 means little — read the answers before believing a flip. Run
validate before learn, or the rules are graded on their own training data.
mecha validate --unprocessed-only
mecha validate --trigger steer,denial --judge-provider anthropic
rules
Rule tenure: ledger tallies per rule, retirement, and staging retirements for rules
the validation ledger keeps convicting. list is the default subcommand.
mecha rules [list|retire|restore|propose-retirements] [ARGS]
| Subcommand | Flag | Description |
|---|---|---|
list | Every rule with its ledger tallies and staleness. | |
retire | <ID> | Retire a rule by id or unique prefix. |
retire | --reason <REASON> | Recorded on the rule and shown to the learner so the lesson does not come back reworded. |
restore | <ID> | Un-retire a rule by id or unique prefix. |
propose-retirements | --min-attributed <N> | Attributed regressions required. Default 3; probationary rules use the shorter threshold of 2. |
propose-retirements | --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 with
no model anywhere; what it stages goes through the same proposal gate as any other
rule change.
mecha rules
mecha rules retire 7c1e --reason "measured harmful on the audit probes"
mecha rules propose-retirements --min-attributed 3
proposals
Review, accept, or reject rule changes staged by mecha learn --propose or
mecha rules propose-retirements. list is the default subcommand.
mecha proposals [list|show|accept|reject] [ARGS]
| 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. |
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.
mecha distill --dry-run
mecha distill -p local --limit 10 --server graph
setup
What this install still needs, and the one command that fixes each.
mecha setup [--json] [--write] [--undecline <STEP_ID>]
| Flag | Description |
|---|---|
--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. |
--undecline <STEP_ID> | Ask about a step you said never to again. all clears every one. Says so when the id was never declined, rather than reporting an undo it did not perform. |
The three are mutually exclusive: each is a different verb, and a pair used to
resolve by whichever branch came first — --json --write printed a plan,
exited 1 and wrote nothing. The parser refuses the combination instead.
Where it differs from doctor, and why both exist: doctor answers
what is silently broken about a working install, in one pass with no network
and no model. Every question setup asks needs to ask a server something,
and its answers change a config file. Folding them together would put a network
call inside the one command whose whole contract is that it has none.
For a local provider it reads GET /props and compares three settings against
what is actually being served — and each is one nothing can check afterwards,
because each degrades quietly rather than failing:
| 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 fix is an API key, and a key is the one thing
this tool will not write. mecha stores the name of an environment variable,
never a secret, which is what makes a config file safe to read, copy and commit.
So the step names the exact variable and both routes forward rather than
offering a command that could only print what you already know. A provider
configured with no api_key_env at all is told that, instead of being told to
set a variable it does not name.
Everything else
It also inventories the integrations — mail, documents, Slack, the knowledge
graph — reporting each as ok, not set up, or unknown, and offering the next
command for the ones it can. And it offers mecha config init when there is no
config file: Config::load_global tolerating its absence is right, since mecha
must work before anybody has written one, and is also exactly why nobody ever
learned about the file that every other step is fixed by editing. Unknown is deliberately not "not set up": a
credential store that could not be read offers nothing, because telling someone
their mail is unconfigured when it is merely unreadable sends them through an
OAuth flow they did not need.
Two things it will not do. The knowledge graph's own sources are named and
never driven — mecha reaches the graph through its MCP tools and nothing
else, and spawning mecha-graph source would be exactly the coupling that rule
prevents. And nothing is scheduled: a trigger runner is offered only once a
trigger already exists, and it is the runner, never a schedule.
It also offers the charter — the ranked standing priorities every run
carries. That step exists because nothing else named the feature to a new
install: doctor correctly says nothing about a charter that has never been
written, since not having one is not a fault, so the only ways to discover it
were the TUI's /help and the web settings gear. The remedy hands over
$EDITOR; nothing here composes a priority.
Saying no, and meaning it
Each offer takes y, N, or never. N is not today and comes back next
time; never records the step id in ~/.mecha/setup-declined.json, and it
then reports as you said no thanks rather than as missing.
That distinction is the point: a declined step is not outstanding, so an
install whose every open question has been answered exits 0 and mecha setup
works as your own health check instead of being permanently red over choices
you already made. --undecline is printed alongside, so the way back is never
something to go looking for.
Three guarantees on it, each structural rather than a matter of the prompt being careful:
- Only genuinely optional things are declinable — the four integrations and
the charter. A provider that cannot answer is not a feature going unused, and
declining it would report
Nothing outstanding.on an install that cannot answer a prompt. - A decline never hides a fault or a fact. It applies only to a step that
is missing: something
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.
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. |
--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> [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. |
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. |
revert | <ID|KEY> | Take an accepted override back out. |
overrides | The active override layer, with where each entry came from. |
ruminate is the nightly verb — diagnose, record, measure by replay, dispose —
and it exits 0 on "nothing to do", because a skipped night is not a failed
night. A config change that wins on selection, is confirmed on the holdout and
holds the work guardrail auto-accepts into the override layer; accept on
anything else only marks the record, since the change itself is yours to make.
A security-class proposal is never measured and never applied.
revert puts the key back to whatever your config says and keeps the candidate
record as evidence.
mecha harness ruminate --days 7
mecha harness list
mecha harness show hc-20260828T0330
mecha harness overrides
mecha harness revert max_turns
See Run quality.
vet
Judge queued knowledge-graph claims against the evidence they were extracted from, and file the verdicts beside them.
mecha vet [OPTIONS]
| 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> / --predicate <P> / --limit <N> / --server <S> | As in vet. |
--since <DATE> | Evidence on/after this date. |
--min-coverage <N> | Minimum episodes a source needs before it counts. |
gossip
Two readers with different sources ask each other about one entity — disagreement between them is the finding, not a failure to converge.
mecha gossip --entity <ENTITY> [OPTIONS]
| Flag | Description |
|---|---|
--entity <E> | The person or project to gossip about — a name, alias, or id. |
--rounds <N> | Rounds of question-and-answer. Bounded on purpose: a preserved disagreement is a finding. |
--since <DATE> | Evidence on/after this date — both readers get the same window, so a difference must be the sources disagreeing, not the world having moved. |
--min-coverage <N> | Minimum episodes a source needs before it can be a vantage. Default 3. |
--verify <N> | Claims to audit after the exchange; 0 skips the audit. |
--adjudicate <N> | Pending claims about this entity to adjudicate after the exchange; 0 skips it. |
--server <S> | The [[mcp]] server holding the graph. Default graph. |
After building context on the entity, the run judges that entity's pending claims — the one output that makes the review backlog smaller rather than larger. Each reader's own searches are marked as instrumentation, so a probe cannot manufacture its own demand signal.
config
Show or create configuration. Requires a subcommand.
mecha config <show|path|init> [OPTIONS]
| 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.