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