Skip to main content

truth_mirror/
cli.rs

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    /// Install, uninstall, or preview agent hook shims.
33    InstallHooks(InstallHooksArgs),
34    /// Review a commit or the staged diff with a separate reviewer model.
35    Review(ReviewArgs),
36    /// Run deterministic repository gates.
37    Gate(GateArgs),
38    /// Reinject unresolved findings into an agent prompt surface.
39    Reinject(ReinjectArgs),
40    /// Inspect or update the dual ledger.
41    Ledger(LedgerArgs),
42    /// Run the post-commit reviewer loop.
43    Watch(WatchArgs),
44    /// Internal git hook dispatcher.
45    #[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    /// Sic the adversarial reviewer in a loop until N lies or N fuckups.
129    #[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    /// Enforcement gate: block a mutating tool call when the ledger has unresolved
165    /// rejections beyond the configured threshold.
166    #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
167    pub pre_tool_use: bool,
168
169    /// The tool name being gated (for `--pre-tool-use`).
170    #[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    /// Drain the review queue exactly once and exit (deterministic; used in CI).
228    #[arg(long)]
229    pub once: bool,
230
231    /// Poll interval in seconds when running as a daemon (ignored with --once).
232    #[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}