truth-mirror 0.4.0

Truthfulness gate and adversarial reviewer harness for AI coding agents.
Documentation
//! Agent reinjection adapters for unresolved findings.

use std::{path::Path, process::ExitCode};

use anyhow::Result;

use crate::{
    cli::{Agent, ReinjectArgs},
    ledger::{LedgerEntry, LedgerStore},
};

pub fn run(args: ReinjectArgs, state_dir: &Path) -> Result<ExitCode> {
    let entries = LedgerStore::new(state_dir).unresolved_rejections()?;
    let output = render(args.agent, &entries);
    if !output.is_empty() {
        print!("{output}");
    }
    Ok(ExitCode::SUCCESS)
}

pub fn render(agent: Agent, entries: &[LedgerEntry]) -> String {
    if entries.is_empty() {
        return String::new();
    }

    let agent_name = match agent {
        Agent::Claude => "Claude",
        Agent::Codex => "Codex",
        Agent::Pi => "Pi",
    };

    let mut output = format!(
        "Truth Mirror unresolved rejections for {agent_name}. Address these before claiming completion or pushing.\n\n"
    );
    for entry in entries {
        output.push_str(&format!(
            "- commit: {}\n  status: {} {}\n  claim: {}\n",
            entry.commit_sha, entry.verdict, entry.disposition, entry.claim
        ));
        if !entry.summary.trim().is_empty() {
            output.push_str(&format!("  summary: {}\n", entry.summary));
        }
        if entry.findings.is_empty() {
            output.push_str("  findings: none\n");
        } else if !entry.structured_findings.is_empty() {
            output.push_str("  findings:\n");
            for finding in &entry.structured_findings {
                output.push_str(&format!("    - {}\n", finding.display_line()));
            }
        } else {
            output.push_str("  findings:\n");
            for finding in &entry.findings {
                output.push_str(&format!("    - {finding}\n"));
            }
        }
        if !entry.next_steps.is_empty() {
            output.push_str("  next steps:\n");
            for step in &entry.next_steps {
                output.push_str(&format!("    - {step}\n"));
            }
        }
        if !entry.raw_reviewer_output.trim().is_empty() {
            output.push_str("  raw reviewer output:\n");
            for line in entry.raw_reviewer_output.trim().lines() {
                output.push_str(&format!("    {line}\n"));
            }
        }
    }
    output
}

#[cfg(test)]
mod tests {
    use crate::{
        cli::Agent,
        ledger::{FindingSeverity, LedgerEntry, ReviewerConfig, StructuredFinding, Verdict},
    };

    use super::render;

    fn rejected_entry() -> LedgerEntry {
        LedgerEntry::new_at(
            "abc123",
            Verdict::Reject,
            "CLAIM: bad | verified: cargo test | evidence: tests:cargo-test",
            vec!["tests:cargo-test".to_owned()],
            ReviewerConfig::new("claude", "opus", false),
            vec!["unsupported".to_owned()],
            100,
        )
    }

    fn structured_entry() -> LedgerEntry {
        rejected_entry().with_structured_review(
            "The claim is unsupported.",
            vec![StructuredFinding {
                severity: FindingSeverity::High,
                title: "unsupported".to_owned(),
                body: "The evidence pointer does not prove the claim.".to_owned(),
                file: "src/lib.rs".to_owned(),
                line_start: 3,
                line_end: 4,
                confidence: 95,
                recommendation: "Add direct evidence.".to_owned(),
            }],
            vec!["Run cargo test.".to_owned()],
            r#"{"verdict":"REJECT"}"#,
        )
    }

    #[test]
    fn reinjection_is_quiet_when_empty() {
        assert_eq!(render(Agent::Claude, &[]), "");
    }

    #[test]
    fn reinjection_mentions_each_supported_agent() {
        for agent in [Agent::Claude, Agent::Codex, Agent::Pi] {
            let output = render(agent, &[rejected_entry()]);

            assert!(output.contains("Truth Mirror unresolved rejections"));
            assert!(output.contains("abc123"));
            assert!(output.contains("unsupported"));
        }
    }

    #[test]
    fn reinjection_includes_structured_provenance() {
        let output = render(Agent::Codex, &[structured_entry()]);

        assert!(output.contains("status: REJECT open"));
        assert!(output.contains("The claim is unsupported."));
        assert!(output.contains("high [src/lib.rs:3-4] unsupported"));
        assert!(output.contains("Run cargo test."));
        assert!(output.contains(r#"{"verdict":"REJECT"}"#));
    }
}