1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
//! `mecha` — an agent harness for local models.
mod approve;
mod commands;
mod editor;
mod interrupt;
mod probe;
mod render;
mod review_policy;
mod setup;
mod slack;
mod tui;
use anyhow::Result;
use clap::{Parser, Subcommand};
use mecha_core::message::Effort;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "mecha",
version,
about = "An agent harness for local models: one loop, any model, native and MCP tools.",
disable_help_subcommand = true
)]
pub struct Cli {
#[command(flatten)]
pub global: GlobalOpts,
#[command(subcommand)]
pub command: Command,
}
/// Options that apply to any command that actually runs an agent.
#[derive(clap::Args, Clone, Debug, Default)]
pub struct GlobalOpts {
/// Provider to use, by config key (default: the config's default_provider).
#[arg(long, short = 'p', global = true)]
pub provider: Option<String>,
/// Model id, overriding the provider's default.
#[arg(long, short = 'm', global = true)]
pub model: Option<String>,
/// Reasoning depth: low, medium, high, xhigh, max.
#[arg(long, short = 'e', global = true)]
pub effort: Option<Effort>,
/// System prompt. Use @path to read it from a file.
#[arg(long, short = 's', global = true)]
pub system: Option<String>,
/// Directory the agent may read and write. Defaults to the working directory.
#[arg(long, short = 'w', global = true)]
pub workspace: Option<PathBuf>,
/// Approve every tool call without asking. Required for unattended runs
/// that need to write or execute anything.
#[arg(long, short = 'y', global = true)]
pub yes: bool,
/// Refuse anything that isn't read-only.
#[arg(long, global = true, conflicts_with = "yes")]
pub read_only: bool,
/// Stop after this many model turns.
#[arg(long, global = true)]
pub max_turns: Option<u32>,
/// Stop once the run has generated this many output tokens.
#[arg(long, global = true)]
pub max_output_tokens: Option<u64>,
/// Stop once the run has cost this much, in USD. Needs prices configured
/// on the provider.
#[arg(long, global = true, value_name = "USD")]
pub max_cost: Option<f64>,
/// Only expose these tools (repeatable). Names are matched exactly.
#[arg(long = "tool", global = true)]
pub tools: Vec<String>,
/// Skip MCP servers entirely.
#[arg(long, global = true)]
pub no_mcp: bool,
/// Skip these MCP servers by name (repeatable). `--no-mcp` skips all of
/// them; this is for turning one off while the rest stay.
#[arg(long = "no-mcp-server", global = true)]
pub no_mcp_servers: Vec<String>,
/// Turn off reasoning. Cheaper and faster, but noticeably worse on
/// multi-step work.
#[arg(long, global = true)]
pub no_thinking: bool,
/// Don't inject learned rules from ~/.mecha/learning into the system
/// prompt.
#[arg(long, global = true)]
pub no_learned_rules: bool,
/// Don't run configured [[hook]] commands.
#[arg(long, global = true)]
pub no_hooks: bool,
/// Don't route any tools through the outbox — configured [outbox] tools
/// execute directly under the usual gates instead of being staged.
#[arg(long, global = true)]
pub no_outbox: bool,
/// No inter-agent messaging: no `message_send` tool, and nothing from
/// the mailbox is delivered into this run.
#[arg(long, global = true)]
pub no_messages: bool,
/// Never fall back to another provider — a configured `fallbacks` list is
/// ignored, and a transient failure that survives its retries fails the
/// run instead of being answered by a different model.
#[arg(long, global = true)]
pub no_fallback: bool,
/// Summarise older turns once the prompt passes this many tokens. Roughly
/// two thirds of the model's context window is a reasonable setting.
#[arg(long, global = true)]
pub compact_at: Option<u64>,
/// Print tool calls, results, and token usage as they happen.
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
/// Not a flag: read `~/.mecha/config.toml` only, ignoring any `mecha.toml`
/// in the working directory.
///
/// Set by the trigger runner, which builds this struct itself rather than
/// parsing it. A scheduled unattended run must not take its MCP servers,
/// hooks or tool surface from whatever repository the daemon happens to
/// have been started in — see [`mecha_core::trigger`].
#[arg(skip)]
pub global_config_only: bool,
}
/// Same shape as `chat`, so switching between them is muscle memory.
#[derive(clap::Args, Debug)]
pub struct TuiArgs {
/// Continue a saved session by id or unique prefix.
#[arg(long)]
pub resume: Option<String>,
/// Don't write a transcript.
#[arg(long)]
pub no_session: bool,
}
#[derive(Subcommand)]
pub enum Command {
/// Run one task and print the answer.
Run(commands::run::Args),
/// Interactive session in the terminal.
Chat(commands::chat::Args),
/// 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.
Tui(TuiArgs),
/// Run the same agent over a JSONL file of prompts.
Batch(commands::batch::Args),
/// Score a model on a case set. The model bake-off rig.
Eval(commands::eval::Args),
/// Mine recorded sessions for user interventions and turn each into a
/// learned reflection.
Reflect(commands::reflect::Args),
/// Absorb unprocessed reflections into the learned rule set.
Learn(commands::learn::Args),
/// Summarise closed sessions into episodes staged to the knowledge graph.
Distill(commands::distill::Args),
/// Probe whether the learned rules change the answers at the recorded
/// moments the user stepped in.
Validate(commands::validate::Args),
/// Review, edit, release, or reject staged outbound actions.
Outbox(commands::outbox::Args),
/// Messages between this machine's agents: send one, read the backlog,
/// see who is running. Delivery happens at the recipient's next turn.
Msg(commands::msg::Args),
/// What runs have generated, and removing what is past. Every producer —
/// a trigger, a chat — writes into its own directory, which is also the
/// path jail its runs get.
Work(commands::work::Args),
/// Read every store — no network, no model, no tokens — and report what
/// is silently wrong: dead mail logins, stuck outbox drafts, stalled
/// frontdoor requests, failing triggers, failed units. On a terminal it
/// offers each remedy; piped, it only reports. Exit 1 when it found
/// anything.
Doctor(commands::doctor::Args),
/// 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. It proposes; it does
/// not measure and does not apply, because a diagnosis is right about
/// which step failed roughly one time in seven and the whole design is
/// arranged so that being wrong costs one measurement.
Diagnose(commands::diagnose::Args),
/// Requests that arrived through the public surface, and the quarantine
/// they pass through before any run with tools is told about them.
/// `factory-publish drain` fetches them; this is what happens next.
Frontdoor(commands::frontdoor::Args),
/// Triage the inbox: classify, read, dismiss.
Mail(commands::mail::Args),
/// Two readers with different sources ask each other about one entity.
Gossip(commands::gossip::GossipArgs),
/// Judge whether queued generalisations hold beyond their one source.
Corroborate(commands::corroborate::CorroborateArgs),
/// Judge queued claims against the evidence they were extracted from.
Vet(commands::vet::VetArgs),
/// Prompts that run on a schedule: a morning briefing, overnight inbox
/// triage, calendar prep. `tick` fires what is due; `daemon` loops it.
Trigger(commands::trigger::Args),
/// Driving mecha from Slack: the tokens, and who is allowed to drive.
/// `auth` stores the credential, `link` binds an owner by a one-time code
/// printed here — which proves shell access to this machine, where an
/// email address proves only what the workspace claims about it.
Slack(commands::slack::Args),
/// Review, accept, or reject rule changes staged by `mecha learn --propose`.
Proposals(commands::proposals::Args),
/// Rule tenure: ledger tallies per rule, retire/restore, and staging
/// retirements for rules the validation ledger keeps convicting.
Rules(commands::rules::Args),
/// Re-run a recorded session against recorded tool results and report
/// where the model diverged.
Replay(commands::replay::Args),
/// List the tools an agent would see.
Tools(commands::tools::Args),
/// Inspect saved transcripts.
#[command(subcommand)]
Sessions(commands::sessions::Args),
/// Show or create configuration.
#[command(subcommand)]
Config(commands::config::Args),
}
#[tokio::main]
async fn main() {
// Quiet by default; `MECHA_LOG=debug` turns on the internals.
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_env("MECHA_LOG")
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")),
)
.with_writer(std::io::stderr)
.without_time()
.init();
if let Err(e) = dispatch().await {
eprintln!("mecha: {e:#}");
std::process::exit(1);
}
}
async fn dispatch() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Run(args) => commands::run::execute(&cli.global, args).await,
Command::Chat(args) => commands::chat::execute(&cli.global, args).await,
Command::Tui(args) => tui::execute(&cli.global, args.resume, args.no_session).await,
Command::Batch(args) => commands::batch::execute(&cli.global, args).await,
Command::Eval(args) => commands::eval::execute(&cli.global, args).await,
Command::Reflect(args) => commands::reflect::execute(&cli.global, args).await,
Command::Learn(args) => commands::learn::execute(&cli.global, args).await,
Command::Distill(args) => commands::distill::execute(&cli.global, args).await,
Command::Validate(args) => commands::validate::execute(&cli.global, args).await,
Command::Outbox(args) => commands::outbox::execute(&cli.global, args).await,
Command::Msg(args) => commands::msg::execute(args).await,
Command::Work(args) => commands::work::execute(args).await,
Command::Doctor(args) => commands::doctor::execute(args).await,
Command::Diagnose(args) => commands::diagnose::execute(&cli.global, args).await,
Command::Frontdoor(args) => commands::frontdoor::run(&cli.global, args).await,
Command::Mail(args) => commands::mail::run(&cli.global, args).await,
Command::Gossip(args) => commands::gossip::run(&cli.global, &args).await,
Command::Corroborate(args) => commands::corroborate::run(&cli.global, &args).await,
Command::Vet(args) => commands::vet::run(&cli.global, &args).await,
Command::Slack(args) => commands::slack::run(&cli.global, args).await,
Command::Trigger(args) => commands::trigger::execute(&cli.global, args).await,
Command::Proposals(args) => commands::proposals::execute(args).await,
Command::Rules(args) => commands::rules::execute(args).await,
Command::Replay(args) => commands::replay::execute(&cli.global, args).await,
Command::Tools(args) => commands::tools::execute(&cli.global, args).await,
Command::Sessions(args) => commands::sessions::execute(&cli.global, args).await,
Command::Config(args) => commands::config::execute(&cli.global, args).await,
}
}