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