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
//! CLI argument parsing.
//!
//! Aligned with `defect-config`'s `LoadConfigOptions::cli` — CLI flags take precedence.
//! CLI arguments — see config and `CliOverrides`.
use clap::{Parser, Subcommand, ValueEnum};
use defect_config::{
CliOverrides, LogFormat, ProviderKind as ConfigProviderKind, SandboxMode, parse_cli_override,
};
/// Values for `--sandbox`. Mirrors [`SandboxMode`] locally so that clap can render the
/// possible values directly;
/// the config crate does not depend on clap, so it does not derive `ValueEnum` there
/// (following the same
/// "CLI-side parsing" pattern used by providers).
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum SandboxModeArg {
ReadOnly,
AskWrites,
Open,
DenyAll,
}
impl From<SandboxModeArg> for SandboxMode {
fn from(arg: SandboxModeArg) -> Self {
match arg {
SandboxModeArg::ReadOnly => SandboxMode::ReadOnly,
SandboxModeArg::AskWrites => SandboxMode::AskWrites,
SandboxModeArg::Open => SandboxMode::Open,
SandboxModeArg::DenyAll => SandboxMode::DenyAll,
}
}
}
/// Values for `--log-format`. Mirrors [`LogFormat`] locally so that clap can render the
/// possible values directly; the config crate does not depend on clap.
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum LogFormatArg {
Text,
Jsonl,
}
impl From<LogFormatArg> for LogFormat {
fn from(arg: LogFormatArg) -> Self {
match arg {
LogFormatArg::Text => LogFormat::Text,
LogFormatArg::Jsonl => LogFormat::Jsonl,
}
}
}
/// Output format for stdout in `--message` single-turn mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum OutputFormat {
/// Plain text, no ANSI: assistant body to stdout, thoughts/tools to stderr.
#[default]
Text,
/// One JSON line (NDJSON) per `AgentEvent` to stdout.
Json,
/// Silent mode; only prints the final result or error at the end.
Quiet,
}
/// Headless agent over ACP/stdio.
#[derive(Debug, Parser)]
#[command(
name = "defect",
version,
about = "Headless agent over ACP/stdio",
long_about = "defect — headless agent over ACP/stdio.\n\n\
Auth env: ANTHROPIC_API_KEY / OPENAI_API_KEY / DEEPSEEK_API_KEY.\n\
Logging: RUST_LOG controls tracing-subscriber EnvFilter (default: info); \
--log-format selects text or jsonl output."
)]
pub struct CliArgs {
/// LLM provider to use. CLI flag takes precedence over the `DEFECT_PROVIDER`
/// environment variable and config file.
#[arg(long, env = "DEFECT_PROVIDER")]
pub provider: Option<String>,
/// Override the default model ID. CLI flag takes precedence over the `DEFECT_MODEL`
/// environment variable.
#[arg(long, env = "DEFECT_MODEL")]
pub model: Option<String>,
/// Override the sandbox permission mode. CLI flag takes precedence over config
/// `[sandbox].mode`. Useful for CI: `--sandbox open` runs every tool without
/// prompting. Note that `--repl` always forces `open` regardless.
#[arg(long, value_enum)]
pub sandbox: Option<SandboxModeArg>,
/// Shortcut for `--sandbox open`: grants maximum permissions and runs every tool
/// without prompting. Mutually exclusive with `--sandbox`.
#[arg(long, conflicts_with = "sandbox")]
pub yolo: bool,
/// Run the entire session under a named subagent profile, located in
/// `.defect/agents/<name>/` or `~/.config/defect/agents/<name>/`.
/// The profile's model, system prompt, and tool allowlist become the session root.
/// The CLI flag takes precedence over the `DEFECT_PROFILE` environment variable.
#[arg(long, env = "DEFECT_PROFILE")]
pub profile: Option<String>,
/// Format for the tracing/log output (stderr). `text` (default) is human-readable;
/// `jsonl` emits one JSON object per log line. Takes precedence over config
/// `[tracing].format`. This controls diagnostic logs, not the `--format` stdout event
/// stream.
#[arg(long, value_enum)]
pub log_format: Option<LogFormatArg>,
/// Additional dotted-path config overrides; may be repeated.
#[arg(long = "config", value_name = "KEY=VALUE")]
pub config_override: Vec<String>,
/// Resume a previous session. With a `SESSION_ID`, resume that specific session; bare
/// `--resume` resumes the most recently active session for the current working
/// directory. In ACP mode, the resumed session is returned on the first
/// `session/new`; in `--repl` mode, it is loaded directly.
#[arg(long, value_name = "SESSION_ID")]
pub resume: Option<Option<String>>,
/// Sandbox mode: ignore global/user config and store all state (config, sessions)
/// under `<repo-root>/.defect/`. The user-level `~/.config/defect` config, agents,
/// and skills are skipped entirely.
#[arg(long)]
pub local: bool,
/// Run a minimal in-process REPL on stdin/stdout instead of the ACP server. Requires
/// the `repl` build feature (enabled by default); a binary built with
/// `--no-default-features` rejects this flag at runtime.
#[arg(long)]
pub repl: bool,
/// Run a single prompt turn headlessly and exit (CI / scripting). The assistant
/// output goes to stdout; the process exit code reflects the turn outcome. A value of
/// `-`, or no value while stdin is piped, reads the prompt from stdin. Combine with
/// `--resume` to continue a previous session. Mutually exclusive with `--repl`.
/// Requires the `oneshot` build feature (on by default).
#[arg(long, value_name = "PROMPT", conflicts_with = "repl")]
pub message: Option<String>,
/// Output format for `--message` / `--goal` mode.
#[arg(long, value_enum, default_value_t = OutputFormat::default())]
pub format: OutputFormat,
/// Run a goal-driven autonomous loop and exit (for CI / scripting). The agent works
/// across multiple turns until it calls the `goal_done` tool (goal achieved) or
/// reaches `--max-turns`. Like `--message`, but continues until the goal is reached
/// instead of stopping after one turn. Reads from stdin if the value is `-` or
/// omitted while piped. Mutually exclusive with `--message` and `--repl`. Requires
/// the `oneshot` build feature.
#[arg(long, value_name = "OBJECTIVE", conflicts_with_all = ["message", "repl"])]
pub goal: Option<String>,
/// Maximum number of times the goal-gate may keep the agent going before giving up
/// (maps to `[turn].max_hook_continues`). Only meaningful with `--goal`. When
/// exceeded, the run exits with a non-zero (exhausted) code.
#[arg(long, value_name = "N")]
pub max_turns: Option<u32>,
/// Optional subcommand. When omitted, `defect` runs as an agent (ACP server by
/// default, or one of the `--repl` / `--message` / `--goal` modes). Subcommands are
/// for one-off management tasks that exit without starting an agent.
#[command(subcommand)]
pub command: Option<Command>,
}
/// Management subcommands that run instead of the agent and then exit.
#[derive(Debug, Subcommand)]
pub enum Command {
/// Scan the environment for provider API keys and write a global config.toml.
Init(InitArgs),
}
/// Arguments for `defect init`.
#[derive(Debug, Default, clap::Args)]
pub struct InitArgs {
/// Non-interactive: skip all prompts and write the config from detected env keys.
/// When multiple provider keys are present, `--default-provider` must also be given
/// (defect never guesses which provider should be the default).
#[arg(long)]
pub yes: bool,
/// Overwrite an existing global config.toml. Without this, init refuses to clobber
/// an existing file.
#[arg(long)]
pub force: bool,
/// The provider to record as `[default] provider`. Required by `--yes` when more
/// than one provider key is detected; otherwise optional (defaults to the sole
/// detected provider, or is chosen interactively).
#[arg(long, value_name = "PROVIDER")]
pub default_provider: Option<String>,
/// The model to record as `[default] model` for the default provider. Must be one of
/// the models the provider's API actually returns (init validates against the live
/// list). When omitted: chosen interactively, or under `--yes` the first model the
/// provider lists.
#[arg(long, value_name = "MODEL")]
pub default_model: Option<String>,
}
impl CliArgs {
/// Translates CLI flags into [`CliOverrides`] and feeds them to
/// `defect_config::load_config`.
pub fn to_overrides(&self) -> anyhow::Result<CliOverrides> {
let config_overrides = self
.config_override
.iter()
.map(|spec| parse_cli_override(spec).map_err(|e| anyhow::anyhow!("{e}")))
.collect::<anyhow::Result<Vec<_>>>()?;
// `--yolo` is syntactic sugar for `--sandbox open` (clap ensures they are
// mutually exclusive).
let sandbox = if self.yolo {
Some(SandboxMode::Open)
} else {
self.sandbox.map(SandboxMode::from)
};
Ok(CliOverrides {
provider: self.provider.as_deref().map(ConfigProviderKind::from),
model: self.model.clone(),
sandbox,
log_format: self.log_format.map(LogFormat::from),
config_overrides,
})
}
}