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 MemorySkill(MemorySkillArgs),
44 Watch(WatchArgs),
46 Status(StatusArgs),
48 Skills(SkillsArgs),
50 #[command(hide = true)]
52 HookDispatch(HookDispatchArgs),
53}
54
55#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
56#[serde(rename_all = "kebab-case")]
57pub enum Agent {
58 Claude,
59 Codex,
60 Pi,
61}
62
63#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
64#[serde(rename_all = "kebab-case")]
65pub enum ReviewerHarness {
66 Claude,
67 Codex,
68 Pi,
69 Gemini,
70 Opencode,
71 Custom,
72}
73
74#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
75#[serde(rename_all = "kebab-case")]
76pub enum ReviewScope {
77 Commit,
78 Staged,
79 Auto,
80 WorkingTree,
81 Branch,
82}
83
84#[derive(Debug, Args)]
85pub struct InstallHooksArgs {
86 #[arg(long)]
87 pub claude: bool,
88
89 #[arg(long)]
90 pub codex: bool,
91
92 #[arg(long)]
93 pub pi: bool,
94
95 #[arg(long)]
96 pub uninstall: bool,
97
98 #[arg(long)]
99 pub dry_run: bool,
100
101 #[arg(long)]
103 pub inject_forwarder: bool,
104}
105
106#[derive(Debug, Args)]
107pub struct ReviewArgs {
108 #[command(subcommand)]
109 pub command: Option<ReviewCommand>,
110
111 #[arg(value_name = "SHA", conflicts_with = "staged")]
112 pub target: Option<String>,
113
114 #[arg(long)]
115 pub staged: bool,
116
117 #[arg(long, value_enum, value_name = "SCOPE", default_value_t = ReviewScope::Commit)]
118 pub scope: ReviewScope,
119
120 #[arg(long, value_name = "REF")]
121 pub base: Option<String>,
122
123 #[arg(long, value_enum, value_name = "AGENT")]
124 pub watched_agent: Option<Agent>,
125
126 #[arg(long, value_enum, value_name = "HARNESS")]
127 pub reviewer_harness: Option<ReviewerHarness>,
128
129 #[arg(long, value_name = "MODEL")]
130 pub watched_model: Option<String>,
131
132 #[arg(long, value_name = "MODEL")]
133 pub reviewer_model: Option<String>,
134
135 #[arg(long, value_enum, value_name = "EFFORT")]
136 pub reviewer_effort: Option<crate::config::Effort>,
137
138 #[arg(long)]
139 pub allow_same_model: bool,
140
141 #[arg(long)]
142 pub strict_two_pass: bool,
143
144 #[arg(long, value_enum, value_name = "HARNESS")]
145 pub arbiter_harness: Option<ReviewerHarness>,
146
147 #[arg(long, value_name = "MODEL")]
148 pub arbiter_model: Option<String>,
149
150 #[arg(long, value_enum, value_name = "EFFORT")]
151 pub arbiter_effort: Option<crate::config::Effort>,
152
153 #[arg(long)]
155 pub strict_goal: bool,
156
157 #[arg(long, value_name = "N")]
158 pub stop_after_lies: Option<u32>,
159
160 #[arg(long, value_name = "N")]
161 pub stop_after_fuckups: Option<u32>,
162
163 #[arg(long, value_name = "N")]
164 pub max_passes: Option<u32>,
165}
166
167#[derive(Debug, Subcommand)]
168pub enum ReviewCommand {
169 Status {
171 #[arg(value_name = "RUN_ID")]
172 run_id: Option<String>,
173 },
174 Result {
176 #[arg(value_name = "RUN_ID")]
177 run_id: Option<String>,
178 },
179 Cancel {
184 #[arg(value_name = "RUN_ID")]
185 run_id: String,
186
187 #[arg(long)]
189 force: bool,
190 },
191}
192
193#[derive(Debug, Args)]
194#[command(group(
195 ArgGroup::new("gate_mode")
196 .required(true)
197 .args(["pre_push", "commit_msg", "pre_tool_use"])
198))]
199pub struct GateArgs {
200 #[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
201 pub pre_push: Option<String>,
202
203 #[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
204 pub commit_msg: Option<PathBuf>,
205
206 #[arg(long, value_name = "FILE", requires = "commit_msg")]
207 pub claim_file: Option<PathBuf>,
208
209 #[arg(long, value_name = "FILE", requires = "commit_msg")]
210 pub diff_file: Option<PathBuf>,
211
212 #[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
213 pub fake_markers: Vec<String>,
214
215 #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
218 pub pre_tool_use: bool,
219
220 #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
222 pub tool: Option<String>,
223}
224
225#[derive(Debug, Args)]
226pub struct ReinjectArgs {
227 #[arg(long, value_enum)]
228 pub agent: Agent,
229}
230
231#[derive(Debug, Args)]
232pub struct LedgerArgs {
233 #[command(subcommand)]
234 pub command: LedgerCommand,
235}
236
237#[derive(Debug, Subcommand)]
238pub enum LedgerCommand {
239 List,
240 Show {
241 #[arg(value_name = "SHA")]
242 sha: String,
243 },
244 Resolve {
245 #[arg(value_name = "SHA")]
246 sha: String,
247 },
248 Waive {
249 #[arg(value_name = "SHA")]
250 sha: String,
251
252 #[arg(long, value_name = "REASON")]
253 reason: String,
254 },
255 Stats,
256}
257
258#[derive(Debug, Args)]
259pub struct MemorySkillArgs {
260 #[command(subcommand)]
261 pub command: MemorySkillCommand,
262}
263
264#[derive(Debug, Subcommand)]
265pub enum MemorySkillCommand {
266 List,
268 Show {
270 #[arg(value_name = "CANDIDATE_ID")]
271 candidate_id: String,
272 },
273 Render {
275 #[arg(value_name = "CANDIDATE_ID")]
276 candidate_id: String,
277 },
278 Approve {
280 #[arg(value_name = "CANDIDATE_ID")]
281 candidate_id: String,
282
283 #[arg(long)]
284 apply: bool,
285 },
286 Apply {
288 #[arg(value_name = "CANDIDATE_ID")]
289 candidate_id: String,
290 },
291 Reject {
293 #[arg(value_name = "CANDIDATE_ID")]
294 candidate_id: String,
295
296 #[arg(long, value_name = "REASON")]
297 reason: String,
298 },
299 Supersede {
301 #[arg(value_name = "CANDIDATE_ID")]
302 candidate_id: String,
303
304 #[arg(long, value_name = "REPLACEMENT_ID")]
305 replacement: String,
306
307 #[arg(long, value_name = "REASON")]
308 reason: String,
309 },
310 DismissAdvisory {
312 #[arg(value_name = "ADVISORY_ID")]
313 advisory_id: String,
314 },
315}
316
317#[derive(Debug, Args)]
318pub struct WatchArgs {
319 #[arg(long, value_enum, value_name = "AGENT")]
320 pub watched_agent: Option<Agent>,
321
322 #[arg(long, value_enum, value_name = "HARNESS")]
323 pub reviewer_harness: Option<ReviewerHarness>,
324
325 #[arg(long, value_name = "MODEL")]
326 pub watched_model: Option<String>,
327
328 #[arg(long, value_name = "MODEL")]
329 pub reviewer_model: Option<String>,
330
331 #[arg(long, value_enum, value_name = "EFFORT")]
332 pub reviewer_effort: Option<crate::config::Effort>,
333
334 #[arg(long)]
335 pub allow_same_model: bool,
336
337 #[arg(long)]
339 pub once: bool,
340
341 #[arg(long, value_name = "SECONDS", default_value_t = 5)]
343 pub poll_secs: u64,
344}
345
346#[derive(Debug, Args)]
347pub struct StatusArgs {}
348
349#[derive(Debug, Args)]
350pub struct SkillsArgs {
351 #[command(subcommand)]
352 pub command: SkillsCommand,
353}
354
355#[derive(Debug, Subcommand)]
356pub enum SkillsCommand {
357 Echo,
359 Install {
361 #[arg(long, value_name = "PATH")]
363 dir: Option<PathBuf>,
364
365 #[arg(long)]
367 force: bool,
368 },
369}
370
371#[derive(Debug, Args)]
372pub struct HookDispatchArgs {
373 #[arg(value_enum)]
374 pub hook: HookName,
375
376 #[arg(value_name = "ARGS")]
377 pub args: Vec<String>,
378}
379
380#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
381#[value(rename_all = "kebab-case")]
382pub enum HookName {
383 CommitMsg,
384 PostCommit,
385 PrePush,
386}
387
388impl HookName {
389 pub fn as_str(self) -> &'static str {
390 match self {
391 Self::CommitMsg => "commit-msg",
392 Self::PostCommit => "post-commit",
393 Self::PrePush => "pre-push",
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use clap::CommandFactory;
401
402 use super::Cli;
403
404 #[test]
405 fn clap_contract_is_valid() {
406 Cli::command().debug_assert();
407 }
408}