truth-mirror 0.2.0

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
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 {
    /// Install, uninstall, or preview agent hook shims.
    InstallHooks(InstallHooksArgs),
    /// Review a commit or the staged diff with a separate reviewer model.
    Review(ReviewArgs),
    /// Run deterministic repository gates.
    Gate(GateArgs),
    /// Reinject unresolved findings into an agent prompt surface.
    Reinject(ReinjectArgs),
    /// Inspect or update the dual ledger.
    Ledger(LedgerArgs),
    /// Run the post-commit reviewer loop.
    Watch(WatchArgs),
    /// Internal git hook dispatcher.
    #[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, value_enum, value_name = "EFFORT")]
    pub reviewer_effort: Option<crate::config::Effort>,

    #[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, value_enum, value_name = "EFFORT")]
    pub arbiter_effort: Option<crate::config::Effort>,

    /// Sic the adversarial reviewer in a loop until N lies or N fuckups.
    #[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", "pre_tool_use"])
))]
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>,

    /// Enforcement gate: block a mutating tool call when the ledger has unresolved
    /// rejections beyond the configured threshold.
    #[arg(long, conflicts_with_all = ["pre_push", "commit_msg"])]
    pub pre_tool_use: bool,

    /// The tool name being gated (for `--pre-tool-use`).
    #[arg(long, value_name = "NAME", requires = "pre_tool_use")]
    pub tool: Option<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, value_enum, value_name = "EFFORT")]
    pub reviewer_effort: Option<crate::config::Effort>,

    #[arg(long)]
    pub allow_same_model: bool,

    /// Drain the review queue exactly once and exit (deterministic; used in CI).
    #[arg(long)]
    pub once: bool,

    /// Poll interval in seconds when running as a daemon (ignored with --once).
    #[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();
    }
}