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    /// Print or install the embedded truth-mirror skill document.
45    Skills(SkillsArgs),
46    /// Internal git hook dispatcher.
47    #[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    /// Sic the adversarial reviewer in a loop until N lies or N fuckups.
131    #[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    /// Enforcement gate: block a mutating tool call when the ledger has unresolved
167    /// rejections beyond the configured threshold.
168    #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
169    pub pre_tool_use: bool,
170
171    /// The tool name being gated (for `--pre-tool-use`).
172    #[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    /// Drain the review queue exactly once and exit (deterministic; used in CI).
230    #[arg(long)]
231    pub once: bool,
232
233    /// Poll interval in seconds when running as a daemon (ignored with --once).
234    #[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    /// Print the embedded skill document to stdout.
247    Echo,
248    /// Write the skill document to <dir>/truth-mirror/SKILL.md.
249    Install {
250        /// Target skills directory (defaults to `.agents/skills`).
251        #[arg(long, value_name = "PATH")]
252        dir: Option<PathBuf>,
253
254        /// Overwrite an existing skill file.
255        #[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}