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