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 Resolve(ResolveArgs),
44 MemorySkill(MemorySkillArgs),
46 Watch(WatchArgs),
48 EnsureWatcher(EnsureWatcherArgs),
50 Status(StatusArgs),
52 Debt(DebtArgs),
54 Tui(TuiArgs),
56 Skills(SkillsArgs),
58 Uninstall(UninstallArgs),
60 #[command(hide = true)]
62 HookDispatch(HookDispatchArgs),
63}
64
65#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
66#[serde(rename_all = "kebab-case")]
67pub enum Agent {
68 Claude,
69 Codex,
70 Pi,
71 Grok,
72}
73
74#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
75#[serde(rename_all = "kebab-case")]
76pub enum ReviewerHarness {
77 Claude,
78 Codex,
79 Pi,
80 Gemini,
81 Opencode,
82 Custom,
83}
84
85#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
86#[serde(rename_all = "kebab-case")]
87pub enum ReviewScope {
88 Commit,
89 Staged,
90 Auto,
91 WorkingTree,
92 Branch,
93}
94
95#[derive(Debug, Args)]
96pub struct InstallHooksArgs {
97 #[arg(long)]
98 pub claude: bool,
99
100 #[arg(long)]
101 pub codex: bool,
102
103 #[arg(long)]
104 pub pi: bool,
105
106 #[arg(long)]
107 pub grok: bool,
108
109 #[arg(long)]
110 pub uninstall: bool,
111
112 #[arg(long)]
113 pub dry_run: bool,
114
115 #[arg(long)]
117 pub inject_forwarder: bool,
118}
119
120#[derive(Debug, Args)]
122pub struct UninstallArgs {
123 #[arg(long)]
125 pub dry_run: bool,
126
127 #[arg(long)]
131 pub purge: bool,
132}
133
134#[derive(Debug, Args)]
135pub struct ReviewArgs {
136 #[command(subcommand)]
137 pub command: Option<ReviewCommand>,
138
139 #[arg(value_name = "SHA", conflicts_with = "staged")]
140 pub target: Option<String>,
141
142 #[arg(long)]
143 pub staged: bool,
144
145 #[arg(long, value_enum, value_name = "SCOPE", default_value_t = ReviewScope::Commit)]
146 pub scope: ReviewScope,
147
148 #[arg(long, value_name = "REF")]
149 pub base: Option<String>,
150
151 #[arg(long, value_enum, value_name = "AGENT")]
152 pub watched_agent: Option<Agent>,
153
154 #[arg(long, value_enum, value_name = "HARNESS")]
155 pub reviewer_harness: Option<ReviewerHarness>,
156
157 #[arg(long, value_name = "MODEL")]
158 pub watched_model: Option<String>,
159
160 #[arg(long, value_name = "MODEL")]
161 pub reviewer_model: Option<String>,
162
163 #[arg(long, value_enum, value_name = "EFFORT")]
164 pub reviewer_effort: Option<crate::config::Effort>,
165
166 #[arg(long)]
167 pub allow_same_model: bool,
168
169 #[arg(long)]
170 pub strict_two_pass: bool,
171
172 #[arg(long, value_enum, value_name = "HARNESS")]
173 pub arbiter_harness: Option<ReviewerHarness>,
174
175 #[arg(long, value_name = "MODEL")]
176 pub arbiter_model: Option<String>,
177
178 #[arg(long, value_enum, value_name = "EFFORT")]
179 pub arbiter_effort: Option<crate::config::Effort>,
180
181 #[arg(long)]
183 pub strict_goal: bool,
184
185 #[arg(long, value_name = "N")]
186 pub stop_after_lies: Option<u32>,
187
188 #[arg(long, value_name = "N")]
189 pub stop_after_fuckups: Option<u32>,
190
191 #[arg(long, value_name = "N")]
192 pub max_passes: Option<u32>,
193}
194
195#[derive(Debug, Subcommand)]
196pub enum ReviewCommand {
197 Status {
199 #[arg(value_name = "RUN_ID")]
200 run_id: Option<String>,
201 },
202 Result {
204 #[arg(value_name = "RUN_ID")]
205 run_id: Option<String>,
206 },
207 Cancel {
212 #[arg(value_name = "RUN_ID")]
213 run_id: String,
214
215 #[arg(long)]
217 force: bool,
218 },
219}
220
221#[derive(Debug, Args)]
222#[command(group(
223 ArgGroup::new("gate_mode")
224 .required(true)
225 .args(["pre_push", "commit_msg", "pre_tool_use"])
226))]
227pub struct GateArgs {
228 #[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
229 pub pre_push: Option<String>,
230
231 #[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
232 pub commit_msg: Option<PathBuf>,
233
234 #[arg(long, value_name = "FILE", requires = "commit_msg")]
235 pub claim_file: Option<PathBuf>,
236
237 #[arg(long, value_name = "FILE", requires = "commit_msg")]
238 pub diff_file: Option<PathBuf>,
239
240 #[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
241 pub fake_markers: Vec<String>,
242
243 #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
246 pub pre_tool_use: bool,
247
248 #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
250 pub tool: Option<String>,
251}
252
253#[derive(Debug, Args)]
254pub struct ReinjectArgs {
255 #[arg(long, value_enum)]
256 pub agent: Agent,
257}
258
259#[derive(Debug, Args)]
260pub struct LedgerArgs {
261 #[command(subcommand)]
262 pub command: LedgerCommand,
263}
264
265#[derive(Debug, Subcommand)]
266pub enum LedgerCommand {
267 List,
268 Show {
269 #[arg(value_name = "SHA")]
270 sha: String,
271 },
272 History {
274 #[arg(value_name = "SHA")]
275 sha: String,
276 },
277 Stats,
278}
279
280#[derive(Debug, Args)]
283pub struct ResolveArgs {
284 #[arg(value_name = "REJECTION_SHA")]
286 pub rejection_sha: String,
287
288 #[arg(long, value_name = "FIX_SHA", conflicts_with = "waive")]
294 pub fixed_by: Option<String>,
295
296 #[arg(long, conflicts_with = "fixed_by", requires = "reason")]
300 pub waive: bool,
301
302 #[arg(long, value_name = "REASON", requires = "waive")]
304 pub reason: Option<String>,
305}
306
307#[derive(Debug, Args)]
308pub struct DebtArgs {}
309
310#[derive(Debug, Args)]
312pub struct TuiArgs {
313 #[arg(long)]
315 pub no_auto_refresh: bool,
316}
317
318#[derive(Debug, Args)]
319pub struct MemorySkillArgs {
320 #[command(subcommand)]
321 pub command: MemorySkillCommand,
322}
323
324#[derive(Debug, Subcommand)]
325pub enum MemorySkillCommand {
326 List,
328 Show {
330 #[arg(value_name = "CANDIDATE_ID")]
331 candidate_id: String,
332 },
333 Render {
335 #[arg(value_name = "CANDIDATE_ID")]
336 candidate_id: String,
337 },
338 Approve {
340 #[arg(value_name = "CANDIDATE_ID")]
341 candidate_id: String,
342
343 #[arg(long)]
344 apply: bool,
345 },
346 Apply {
348 #[arg(value_name = "CANDIDATE_ID")]
349 candidate_id: String,
350 },
351 Reject {
353 #[arg(value_name = "CANDIDATE_ID")]
354 candidate_id: String,
355
356 #[arg(long, value_name = "REASON")]
357 reason: String,
358 },
359 Supersede {
361 #[arg(value_name = "CANDIDATE_ID")]
362 candidate_id: String,
363
364 #[arg(long, value_name = "REPLACEMENT_ID")]
365 replacement: String,
366
367 #[arg(long, value_name = "REASON")]
368 reason: String,
369 },
370 DismissAdvisory {
372 #[arg(value_name = "ADVISORY_ID")]
373 advisory_id: String,
374 },
375}
376
377#[derive(Debug, Args)]
378pub struct WatchArgs {
379 #[arg(long, value_enum, value_name = "AGENT")]
380 pub watched_agent: Option<Agent>,
381
382 #[arg(long, value_enum, value_name = "HARNESS")]
383 pub reviewer_harness: Option<ReviewerHarness>,
384
385 #[arg(long, value_name = "MODEL")]
386 pub watched_model: Option<String>,
387
388 #[arg(long, value_name = "MODEL")]
389 pub reviewer_model: Option<String>,
390
391 #[arg(long, value_enum, value_name = "EFFORT")]
392 pub reviewer_effort: Option<crate::config::Effort>,
393
394 #[arg(long)]
395 pub allow_same_model: bool,
396
397 #[arg(long, conflicts_with = "until_empty")]
402 pub once: bool,
403
404 #[arg(long)]
407 pub until_empty: bool,
408
409 #[arg(long, value_name = "SECONDS", default_value_t = 60)]
412 pub grace: u64,
413
414 #[arg(long, value_name = "SECONDS", default_value_t = 5)]
416 pub poll_secs: u64,
417
418 #[arg(long)]
422 pub wait_for_lock: bool,
423
424 #[arg(long, hide = true)]
429 pub expect_lock_handoff: bool,
430}
431
432#[derive(Debug, Args)]
433pub struct EnsureWatcherArgs {}
434
435#[derive(Debug, Args)]
436pub struct StatusArgs {}
437
438#[derive(Debug, Args)]
439pub struct SkillsArgs {
440 #[command(subcommand)]
441 pub command: SkillsCommand,
442}
443
444#[derive(Debug, Subcommand)]
445pub enum SkillsCommand {
446 Echo,
448 Install {
450 #[arg(long, value_name = "PATH")]
452 dir: Option<PathBuf>,
453
454 #[arg(long)]
456 force: bool,
457 },
458}
459
460#[derive(Debug, Args)]
461pub struct HookDispatchArgs {
462 #[arg(value_enum)]
463 pub hook: HookName,
464
465 #[arg(value_name = "ARGS")]
466 pub args: Vec<String>,
467}
468
469#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
470#[value(rename_all = "kebab-case")]
471pub enum HookName {
472 CommitMsg,
473 PostCommit,
474 PrePush,
475}
476
477impl HookName {
478 pub fn as_str(self) -> &'static str {
479 match self {
480 Self::CommitMsg => "commit-msg",
481 Self::PostCommit => "post-commit",
482 Self::PrePush => "pre-push",
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use clap::CommandFactory;
490
491 use super::Cli;
492
493 #[test]
494 fn clap_contract_is_valid() {
495 Cli::command().debug_assert();
496 }
497}