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 #[command(hide = true)]
46 HookDispatch(HookDispatchArgs),
47}
48
49#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
50#[serde(rename_all = "kebab-case")]
51pub enum Agent {
52 Claude,
53 Codex,
54 Pi,
55}
56
57#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
58#[serde(rename_all = "kebab-case")]
59pub enum ReviewerHarness {
60 Claude,
61 Codex,
62 Pi,
63 Gemini,
64 Opencode,
65 Custom,
66}
67
68#[derive(Debug, Args)]
69pub struct InstallHooksArgs {
70 #[arg(long)]
71 pub claude: bool,
72
73 #[arg(long)]
74 pub codex: bool,
75
76 #[arg(long)]
77 pub pi: bool,
78
79 #[arg(long)]
80 pub uninstall: bool,
81
82 #[arg(long)]
83 pub dry_run: bool,
84}
85
86#[derive(Debug, Args)]
87pub struct ReviewArgs {
88 #[arg(
89 value_name = "SHA",
90 required_unless_present = "staged",
91 conflicts_with = "staged"
92 )]
93 pub target: Option<String>,
94
95 #[arg(long)]
96 pub staged: bool,
97
98 #[arg(long, value_enum, value_name = "AGENT")]
99 pub watched_agent: Option<Agent>,
100
101 #[arg(long, value_enum, value_name = "HARNESS")]
102 pub reviewer_harness: Option<ReviewerHarness>,
103
104 #[arg(long, value_name = "MODEL")]
105 pub watched_model: Option<String>,
106
107 #[arg(long, value_name = "MODEL")]
108 pub reviewer_model: Option<String>,
109
110 #[arg(long, value_enum, value_name = "EFFORT")]
111 pub reviewer_effort: Option<crate::config::Effort>,
112
113 #[arg(long)]
114 pub allow_same_model: bool,
115
116 #[arg(long)]
117 pub strict_two_pass: bool,
118
119 #[arg(long, value_enum, value_name = "HARNESS")]
120 pub arbiter_harness: Option<ReviewerHarness>,
121
122 #[arg(long, value_name = "MODEL")]
123 pub arbiter_model: Option<String>,
124
125 #[arg(long, value_enum, value_name = "EFFORT")]
126 pub arbiter_effort: Option<crate::config::Effort>,
127
128 #[arg(long)]
130 pub strict_goal: bool,
131
132 #[arg(long, value_name = "N")]
133 pub stop_after_lies: Option<u32>,
134
135 #[arg(long, value_name = "N")]
136 pub stop_after_fuckups: Option<u32>,
137
138 #[arg(long, value_name = "N")]
139 pub max_passes: Option<u32>,
140}
141
142#[derive(Debug, Args)]
143#[command(group(
144 ArgGroup::new("gate_mode")
145 .required(true)
146 .args(["pre_push", "commit_msg", "pre_tool_use"])
147))]
148pub struct GateArgs {
149 #[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
150 pub pre_push: Option<String>,
151
152 #[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
153 pub commit_msg: Option<PathBuf>,
154
155 #[arg(long, value_name = "FILE", requires = "commit_msg")]
156 pub claim_file: Option<PathBuf>,
157
158 #[arg(long, value_name = "FILE", requires = "commit_msg")]
159 pub diff_file: Option<PathBuf>,
160
161 #[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
162 pub fake_markers: Vec<String>,
163
164 #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
167 pub pre_tool_use: bool,
168
169 #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
171 pub tool: Option<String>,
172}
173
174#[derive(Debug, Args)]
175pub struct ReinjectArgs {
176 #[arg(long, value_enum)]
177 pub agent: Agent,
178}
179
180#[derive(Debug, Args)]
181pub struct LedgerArgs {
182 #[command(subcommand)]
183 pub command: LedgerCommand,
184}
185
186#[derive(Debug, Subcommand)]
187pub enum LedgerCommand {
188 List,
189 Show {
190 #[arg(value_name = "SHA")]
191 sha: String,
192 },
193 Resolve {
194 #[arg(value_name = "SHA")]
195 sha: String,
196 },
197 Waive {
198 #[arg(value_name = "SHA")]
199 sha: String,
200
201 #[arg(long, value_name = "REASON")]
202 reason: String,
203 },
204 Stats,
205}
206
207#[derive(Debug, Args)]
208pub struct WatchArgs {
209 #[arg(long, value_enum, value_name = "AGENT")]
210 pub watched_agent: Option<Agent>,
211
212 #[arg(long, value_enum, value_name = "HARNESS")]
213 pub reviewer_harness: Option<ReviewerHarness>,
214
215 #[arg(long, value_name = "MODEL")]
216 pub watched_model: Option<String>,
217
218 #[arg(long, value_name = "MODEL")]
219 pub reviewer_model: Option<String>,
220
221 #[arg(long, value_enum, value_name = "EFFORT")]
222 pub reviewer_effort: Option<crate::config::Effort>,
223
224 #[arg(long)]
225 pub allow_same_model: bool,
226
227 #[arg(long)]
229 pub once: bool,
230
231 #[arg(long, value_name = "SECONDS", default_value_t = 5)]
233 pub poll_secs: u64,
234}
235
236#[derive(Debug, Args)]
237pub struct HookDispatchArgs {
238 #[arg(value_enum)]
239 pub hook: HookName,
240
241 #[arg(value_name = "ARGS")]
242 pub args: Vec<String>,
243}
244
245#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
246#[value(rename_all = "kebab-case")]
247pub enum HookName {
248 CommitMsg,
249 PostCommit,
250 PrePush,
251}
252
253impl HookName {
254 pub fn as_str(self) -> &'static str {
255 match self {
256 Self::CommitMsg => "commit-msg",
257 Self::PostCommit => "post-commit",
258 Self::PrePush => "pre-push",
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use clap::CommandFactory;
266
267 use super::Cli;
268
269 #[test]
270 fn clap_contract_is_valid() {
271 Cli::command().debug_assert();
272 }
273}