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-mirror",
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 Watch(WatchArgs),
44 Skills(SkillsArgs),
46 #[command(hide = true)]
48 HookDispatch(HookDispatchArgs),
49}
50
51#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
52#[serde(rename_all = "kebab-case")]
53pub enum Agent {
54 Claude,
55 Codex,
56 Pi,
57}
58
59#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
60#[serde(rename_all = "kebab-case")]
61pub enum ReviewerHarness {
62 Claude,
63 Codex,
64 Pi,
65 Gemini,
66 Opencode,
67 Custom,
68}
69
70#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
71#[serde(rename_all = "kebab-case")]
72pub enum ReviewScope {
73 Commit,
74 Staged,
75 Auto,
76 WorkingTree,
77 Branch,
78}
79
80#[derive(Debug, Args)]
81pub struct InstallHooksArgs {
82 #[arg(long)]
83 pub claude: bool,
84
85 #[arg(long)]
86 pub codex: bool,
87
88 #[arg(long)]
89 pub pi: bool,
90
91 #[arg(long)]
92 pub uninstall: bool,
93
94 #[arg(long)]
95 pub dry_run: bool,
96}
97
98#[derive(Debug, Args)]
99pub struct ReviewArgs {
100 #[command(subcommand)]
101 pub command: Option<ReviewCommand>,
102
103 #[arg(value_name = "SHA", conflicts_with = "staged")]
104 pub target: Option<String>,
105
106 #[arg(long)]
107 pub staged: bool,
108
109 #[arg(long, value_enum, value_name = "SCOPE", default_value_t = ReviewScope::Commit)]
110 pub scope: ReviewScope,
111
112 #[arg(long, value_name = "REF")]
113 pub base: Option<String>,
114
115 #[arg(long, value_enum, value_name = "AGENT")]
116 pub watched_agent: Option<Agent>,
117
118 #[arg(long, value_enum, value_name = "HARNESS")]
119 pub reviewer_harness: Option<ReviewerHarness>,
120
121 #[arg(long, value_name = "MODEL")]
122 pub watched_model: Option<String>,
123
124 #[arg(long, value_name = "MODEL")]
125 pub reviewer_model: Option<String>,
126
127 #[arg(long, value_enum, value_name = "EFFORT")]
128 pub reviewer_effort: Option<crate::config::Effort>,
129
130 #[arg(long)]
131 pub allow_same_model: bool,
132
133 #[arg(long)]
134 pub strict_two_pass: bool,
135
136 #[arg(long, value_enum, value_name = "HARNESS")]
137 pub arbiter_harness: Option<ReviewerHarness>,
138
139 #[arg(long, value_name = "MODEL")]
140 pub arbiter_model: Option<String>,
141
142 #[arg(long, value_enum, value_name = "EFFORT")]
143 pub arbiter_effort: Option<crate::config::Effort>,
144
145 #[arg(long)]
147 pub strict_goal: bool,
148
149 #[arg(long, value_name = "N")]
150 pub stop_after_lies: Option<u32>,
151
152 #[arg(long, value_name = "N")]
153 pub stop_after_fuckups: Option<u32>,
154
155 #[arg(long, value_name = "N")]
156 pub max_passes: Option<u32>,
157}
158
159#[derive(Debug, Subcommand)]
160pub enum ReviewCommand {
161 Status {
163 #[arg(value_name = "RUN_ID")]
164 run_id: Option<String>,
165 },
166 Result {
168 #[arg(value_name = "RUN_ID")]
169 run_id: Option<String>,
170 },
171 Cancel {
176 #[arg(value_name = "RUN_ID")]
177 run_id: String,
178
179 #[arg(long)]
181 force: bool,
182 },
183}
184
185#[derive(Debug, Args)]
186#[command(group(
187 ArgGroup::new("gate_mode")
188 .required(true)
189 .args(["pre_push", "commit_msg", "pre_tool_use"])
190))]
191pub struct GateArgs {
192 #[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
193 pub pre_push: Option<String>,
194
195 #[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
196 pub commit_msg: Option<PathBuf>,
197
198 #[arg(long, value_name = "FILE", requires = "commit_msg")]
199 pub claim_file: Option<PathBuf>,
200
201 #[arg(long, value_name = "FILE", requires = "commit_msg")]
202 pub diff_file: Option<PathBuf>,
203
204 #[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
205 pub fake_markers: Vec<String>,
206
207 #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
210 pub pre_tool_use: bool,
211
212 #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
214 pub tool: Option<String>,
215}
216
217#[derive(Debug, Args)]
218pub struct ReinjectArgs {
219 #[arg(long, value_enum)]
220 pub agent: Agent,
221}
222
223#[derive(Debug, Args)]
224pub struct LedgerArgs {
225 #[command(subcommand)]
226 pub command: LedgerCommand,
227}
228
229#[derive(Debug, Subcommand)]
230pub enum LedgerCommand {
231 List,
232 Show {
233 #[arg(value_name = "SHA")]
234 sha: String,
235 },
236 Resolve {
237 #[arg(value_name = "SHA")]
238 sha: String,
239 },
240 Waive {
241 #[arg(value_name = "SHA")]
242 sha: String,
243
244 #[arg(long, value_name = "REASON")]
245 reason: String,
246 },
247 Stats,
248}
249
250#[derive(Debug, Args)]
251pub struct WatchArgs {
252 #[arg(long, value_enum, value_name = "AGENT")]
253 pub watched_agent: Option<Agent>,
254
255 #[arg(long, value_enum, value_name = "HARNESS")]
256 pub reviewer_harness: Option<ReviewerHarness>,
257
258 #[arg(long, value_name = "MODEL")]
259 pub watched_model: Option<String>,
260
261 #[arg(long, value_name = "MODEL")]
262 pub reviewer_model: Option<String>,
263
264 #[arg(long, value_enum, value_name = "EFFORT")]
265 pub reviewer_effort: Option<crate::config::Effort>,
266
267 #[arg(long)]
268 pub allow_same_model: bool,
269
270 #[arg(long)]
272 pub once: bool,
273
274 #[arg(long, value_name = "SECONDS", default_value_t = 5)]
276 pub poll_secs: u64,
277}
278
279#[derive(Debug, Args)]
280pub struct SkillsArgs {
281 #[command(subcommand)]
282 pub command: SkillsCommand,
283}
284
285#[derive(Debug, Subcommand)]
286pub enum SkillsCommand {
287 Echo,
289 Install {
291 #[arg(long, value_name = "PATH")]
293 dir: Option<PathBuf>,
294
295 #[arg(long)]
297 force: bool,
298 },
299}
300
301#[derive(Debug, Args)]
302pub struct HookDispatchArgs {
303 #[arg(value_enum)]
304 pub hook: HookName,
305
306 #[arg(value_name = "ARGS")]
307 pub args: Vec<String>,
308}
309
310#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
311#[value(rename_all = "kebab-case")]
312pub enum HookName {
313 CommitMsg,
314 PostCommit,
315 PrePush,
316}
317
318impl HookName {
319 pub fn as_str(self) -> &'static str {
320 match self {
321 Self::CommitMsg => "commit-msg",
322 Self::PostCommit => "post-commit",
323 Self::PrePush => "pre-push",
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use clap::CommandFactory;
331
332 use super::Cli;
333
334 #[test]
335 fn clap_contract_is_valid() {
336 Cli::command().debug_assert();
337 }
338}