1use std::path::PathBuf;
2
3use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Parser)]
7#[command(
8 name = "truth-mirror",
9 version,
10 about = "Truthfulness gate and reviewer harness for coding agents.",
11 propagate_version = true
12)]
13pub struct Cli {
14 #[arg(
15 long,
16 global = true,
17 env = "TRUTH_MIRROR_STATE_DIR",
18 default_value = ".truth",
19 value_name = "DIR"
20 )]
21 pub state_dir: PathBuf,
22
23 #[arg(long, global = true, env = "TRUTH_MIRROR_CONFIG", value_name = "FILE")]
24 pub config: Option<PathBuf>,
25
26 #[command(subcommand)]
27 pub command: Commands,
28}
29
30#[derive(Debug, Subcommand)]
31pub enum Commands {
32 InstallHooks(InstallHooksArgs),
34 Review(ReviewArgs),
36 Gate(GateArgs),
38 Reinject(ReinjectArgs),
40 Ledger(LedgerArgs),
42 Watch(WatchArgs),
44 Skills(SkillsArgs),
46 #[command(hide = true)]
48 HookDispatch(HookDispatchArgs),
49}
50
51#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
52#[serde(rename_all = "kebab-case")]
53pub enum Agent {
54 Claude,
55 Codex,
56 Pi,
57}
58
59#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
60#[serde(rename_all = "kebab-case")]
61pub enum ReviewerHarness {
62 Claude,
63 Codex,
64 Pi,
65 Gemini,
66 Opencode,
67 Custom,
68}
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
71#[serde(rename_all = "kebab-case")]
72pub enum ReviewScope {
73 Commit,
74 Staged,
75 Auto,
76 WorkingTree,
77 Branch,
78}
79
80#[derive(Debug, Args)]
81pub struct InstallHooksArgs {
82 #[arg(long)]
83 pub claude: bool,
84
85 #[arg(long)]
86 pub codex: bool,
87
88 #[arg(long)]
89 pub pi: bool,
90
91 #[arg(long)]
92 pub uninstall: bool,
93
94 #[arg(long)]
95 pub dry_run: bool,
96
97 #[arg(long)]
99 pub inject_forwarder: bool,
100}
101
102#[derive(Debug, Args)]
103pub struct ReviewArgs {
104 #[command(subcommand)]
105 pub command: Option<ReviewCommand>,
106
107 #[arg(value_name = "SHA", conflicts_with = "staged")]
108 pub target: Option<String>,
109
110 #[arg(long)]
111 pub staged: bool,
112
113 #[arg(long, value_enum, value_name = "SCOPE", default_value_t = ReviewScope::Commit)]
114 pub scope: ReviewScope,
115
116 #[arg(long, value_name = "REF")]
117 pub base: Option<String>,
118
119 #[arg(long, value_enum, value_name = "AGENT")]
120 pub watched_agent: Option<Agent>,
121
122 #[arg(long, value_enum, value_name = "HARNESS")]
123 pub reviewer_harness: Option<ReviewerHarness>,
124
125 #[arg(long, value_name = "MODEL")]
126 pub watched_model: Option<String>,
127
128 #[arg(long, value_name = "MODEL")]
129 pub reviewer_model: Option<String>,
130
131 #[arg(long, value_enum, value_name = "EFFORT")]
132 pub reviewer_effort: Option<crate::config::Effort>,
133
134 #[arg(long)]
135 pub allow_same_model: bool,
136
137 #[arg(long)]
138 pub strict_two_pass: bool,
139
140 #[arg(long, value_enum, value_name = "HARNESS")]
141 pub arbiter_harness: Option<ReviewerHarness>,
142
143 #[arg(long, value_name = "MODEL")]
144 pub arbiter_model: Option<String>,
145
146 #[arg(long, value_enum, value_name = "EFFORT")]
147 pub arbiter_effort: Option<crate::config::Effort>,
148
149 #[arg(long)]
151 pub strict_goal: bool,
152
153 #[arg(long, value_name = "N")]
154 pub stop_after_lies: Option<u32>,
155
156 #[arg(long, value_name = "N")]
157 pub stop_after_fuckups: Option<u32>,
158
159 #[arg(long, value_name = "N")]
160 pub max_passes: Option<u32>,
161}
162
163#[derive(Debug, Subcommand)]
164pub enum ReviewCommand {
165 Status {
167 #[arg(value_name = "RUN_ID")]
168 run_id: Option<String>,
169 },
170 Result {
172 #[arg(value_name = "RUN_ID")]
173 run_id: Option<String>,
174 },
175 Cancel {
180 #[arg(value_name = "RUN_ID")]
181 run_id: String,
182
183 #[arg(long)]
185 force: bool,
186 },
187}
188
189#[derive(Debug, Args)]
190#[command(group(
191 ArgGroup::new("gate_mode")
192 .required(true)
193 .args(["pre_push", "commit_msg", "pre_tool_use"])
194))]
195pub struct GateArgs {
196 #[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
197 pub pre_push: Option<String>,
198
199 #[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
200 pub commit_msg: Option<PathBuf>,
201
202 #[arg(long, value_name = "FILE", requires = "commit_msg")]
203 pub claim_file: Option<PathBuf>,
204
205 #[arg(long, value_name = "FILE", requires = "commit_msg")]
206 pub diff_file: Option<PathBuf>,
207
208 #[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
209 pub fake_markers: Vec<String>,
210
211 #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
214 pub pre_tool_use: bool,
215
216 #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
218 pub tool: Option<String>,
219}
220
221#[derive(Debug, Args)]
222pub struct ReinjectArgs {
223 #[arg(long, value_enum)]
224 pub agent: Agent,
225}
226
227#[derive(Debug, Args)]
228pub struct LedgerArgs {
229 #[command(subcommand)]
230 pub command: LedgerCommand,
231}
232
233#[derive(Debug, Subcommand)]
234pub enum LedgerCommand {
235 List,
236 Show {
237 #[arg(value_name = "SHA")]
238 sha: String,
239 },
240 Resolve {
241 #[arg(value_name = "SHA")]
242 sha: String,
243 },
244 Waive {
245 #[arg(value_name = "SHA")]
246 sha: String,
247
248 #[arg(long, value_name = "REASON")]
249 reason: String,
250 },
251 Stats,
252}
253
254#[derive(Debug, Args)]
255pub struct WatchArgs {
256 #[arg(long, value_enum, value_name = "AGENT")]
257 pub watched_agent: Option<Agent>,
258
259 #[arg(long, value_enum, value_name = "HARNESS")]
260 pub reviewer_harness: Option<ReviewerHarness>,
261
262 #[arg(long, value_name = "MODEL")]
263 pub watched_model: Option<String>,
264
265 #[arg(long, value_name = "MODEL")]
266 pub reviewer_model: Option<String>,
267
268 #[arg(long, value_enum, value_name = "EFFORT")]
269 pub reviewer_effort: Option<crate::config::Effort>,
270
271 #[arg(long)]
272 pub allow_same_model: bool,
273
274 #[arg(long)]
276 pub once: bool,
277
278 #[arg(long, value_name = "SECONDS", default_value_t = 5)]
280 pub poll_secs: u64,
281}
282
283#[derive(Debug, Args)]
284pub struct SkillsArgs {
285 #[command(subcommand)]
286 pub command: SkillsCommand,
287}
288
289#[derive(Debug, Subcommand)]
290pub enum SkillsCommand {
291 Echo,
293 Install {
295 #[arg(long, value_name = "PATH")]
297 dir: Option<PathBuf>,
298
299 #[arg(long)]
301 force: bool,
302 },
303}
304
305#[derive(Debug, Args)]
306pub struct HookDispatchArgs {
307 #[arg(value_enum)]
308 pub hook: HookName,
309
310 #[arg(value_name = "ARGS")]
311 pub args: Vec<String>,
312}
313
314#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
315#[value(rename_all = "kebab-case")]
316pub enum HookName {
317 CommitMsg,
318 PostCommit,
319 PrePush,
320}
321
322impl HookName {
323 pub fn as_str(self) -> &'static str {
324 match self {
325 Self::CommitMsg => "commit-msg",
326 Self::PostCommit => "post-commit",
327 Self::PrePush => "pre-push",
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use clap::CommandFactory;
335
336 use super::Cli;
337
338 #[test]
339 fn clap_contract_is_valid() {
340 Cli::command().debug_assert();
341 }
342}