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