use std::path::PathBuf;
use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
#[derive(Debug, Parser)]
#[command(
name = "truth-mirror",
version,
about = "Truthfulness gate and reviewer harness for coding agents.",
propagate_version = true
)]
pub struct Cli {
#[arg(
long,
global = true,
env = "TRUTH_MIRROR_STATE_DIR",
default_value = ".truth-mirror",
value_name = "DIR"
)]
pub state_dir: PathBuf,
#[arg(long, global = true, env = "TRUTH_MIRROR_CONFIG", value_name = "FILE")]
pub config: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
InstallHooks(InstallHooksArgs),
Review(ReviewArgs),
Gate(GateArgs),
Reinject(ReinjectArgs),
Ledger(LedgerArgs),
Watch(WatchArgs),
#[command(hide = true)]
HookDispatch(HookDispatchArgs),
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Agent {
Claude,
Codex,
Pi,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum ReviewerHarness {
Claude,
Codex,
Pi,
Gemini,
Opencode,
Custom,
}
#[derive(Debug, Args)]
pub struct InstallHooksArgs {
#[arg(long)]
pub claude: bool,
#[arg(long)]
pub codex: bool,
#[arg(long)]
pub pi: bool,
#[arg(long)]
pub uninstall: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct ReviewArgs {
#[arg(
value_name = "SHA",
required_unless_present = "staged",
conflicts_with = "staged"
)]
pub target: Option<String>,
#[arg(long)]
pub staged: bool,
#[arg(long, value_enum, value_name = "AGENT")]
pub watched_agent: Option<Agent>,
#[arg(long, value_enum, value_name = "HARNESS")]
pub reviewer_harness: Option<ReviewerHarness>,
#[arg(long, value_name = "MODEL")]
pub watched_model: Option<String>,
#[arg(long, value_name = "MODEL")]
pub reviewer_model: Option<String>,
#[arg(long)]
pub allow_same_model: bool,
#[arg(long)]
pub strict_two_pass: bool,
#[arg(long, value_enum, value_name = "HARNESS")]
pub arbiter_harness: Option<ReviewerHarness>,
#[arg(long, value_name = "MODEL")]
pub arbiter_model: Option<String>,
#[arg(long)]
pub strict_goal: bool,
#[arg(long, value_name = "N")]
pub stop_after_lies: Option<u32>,
#[arg(long, value_name = "N")]
pub stop_after_fuckups: Option<u32>,
#[arg(long, value_name = "N")]
pub max_passes: Option<u32>,
}
#[derive(Debug, Args)]
#[command(group(
ArgGroup::new("gate_mode")
.required(true)
.args(["pre_push", "commit_msg"])
))]
pub struct GateArgs {
#[arg(long, value_name = "RANGE", conflicts_with = "commit_msg")]
pub pre_push: Option<String>,
#[arg(long, value_name = "FILE", conflicts_with = "pre_push")]
pub commit_msg: Option<PathBuf>,
#[arg(long, value_name = "FILE", requires = "commit_msg")]
pub claim_file: Option<PathBuf>,
#[arg(long, value_name = "FILE", requires = "commit_msg")]
pub diff_file: Option<PathBuf>,
#[arg(long = "fake-marker", value_name = "TOKEN", requires = "commit_msg")]
pub fake_markers: Vec<String>,
}
#[derive(Debug, Args)]
pub struct ReinjectArgs {
#[arg(long, value_enum)]
pub agent: Agent,
}
#[derive(Debug, Args)]
pub struct LedgerArgs {
#[command(subcommand)]
pub command: LedgerCommand,
}
#[derive(Debug, Subcommand)]
pub enum LedgerCommand {
List,
Show {
#[arg(value_name = "SHA")]
sha: String,
},
Resolve {
#[arg(value_name = "SHA")]
sha: String,
},
Waive {
#[arg(value_name = "SHA")]
sha: String,
#[arg(long, value_name = "REASON")]
reason: String,
},
Stats,
}
#[derive(Debug, Args)]
pub struct WatchArgs {
#[arg(long, value_enum, value_name = "AGENT")]
pub watched_agent: Option<Agent>,
#[arg(long, value_enum, value_name = "HARNESS")]
pub reviewer_harness: Option<ReviewerHarness>,
#[arg(long, value_name = "MODEL")]
pub watched_model: Option<String>,
#[arg(long, value_name = "MODEL")]
pub reviewer_model: Option<String>,
#[arg(long)]
pub allow_same_model: bool,
#[arg(long)]
pub once: bool,
#[arg(long, value_name = "SECONDS", default_value_t = 5)]
pub poll_secs: u64,
}
#[derive(Debug, Args)]
pub struct HookDispatchArgs {
#[arg(value_enum)]
pub hook: HookName,
#[arg(value_name = "ARGS")]
pub args: Vec<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum HookName {
CommitMsg,
PostCommit,
PrePush,
}
impl HookName {
pub fn as_str(self) -> &'static str {
match self {
Self::CommitMsg => "commit-msg",
Self::PostCommit => "post-commit",
Self::PrePush => "pre-push",
}
}
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::Cli;
#[test]
fn clap_contract_is_valid() {
Cli::command().debug_assert();
}
}