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"}"#));
}
}