use crate::cli::Invocation;
use crate::git;
use crate::state;
use crate::trailer::{self, key_for, Verdict, COUNTS_SHAPE};
pub fn skipped(gate: &str, paths: &[String]) {
eprintln!(
"git-agent-verdict: {gate}: skipped — no staged file matches {}",
paths.join(", ")
);
}
fn shape(inv: &Invocation) -> String {
let key = key_for(&inv.gate);
format!("{key}: reviewer=<id> {COUNTS_SHAPE} token=<issued>")
}
pub fn missing(inv: &Invocation, detail: &str) {
eprintln!(
"\ngit-agent-verdict: error: {}: no reviewable trailer\n",
inv.gate
);
eprintln!(" missing: {detail}");
eprintln!(" wanted: {}\n", shape(inv));
eprintln!(" git agent-verdict attest --intent \"<the aim, one flat line>\"\n");
eprintln!(" - runs each gate in turn, records what the reviewer reported");
eprintln!(" - commits once every gate is attested");
eprintln!(" - composes the message from --intent; this message file is discarded");
}
fn summarize(verdicts: &[Verdict]) -> String {
trailer::total(verdicts).render()
}
pub fn attested(gate: &str, count: usize, verdicts: &[Verdict]) {
eprintln!(
"git-agent-verdict: {gate}: attested ({count} verdict(s), {})",
summarize(verdicts)
);
}
pub fn blocked(gate: &str, major: u32) {
eprintln!("\ngit-agent-verdict: error: {gate}: declared blocker");
eprintln!(" major={major} (must be 0)");
eprintln!(" - fix what the review named; the remedy is yours");
eprintln!(" - the gate reopens only on a fresh attest of {gate}");
}
pub fn untraceable(gate: &str, token: &str) {
eprintln!("\ngit-agent-verdict: error: {gate}: unknown token\n");
eprintln!(" token={token} matches no review recorded for this HEAD");
eprintln!(" - run: git agent-verdict attest --intent \"…\"");
eprintln!(" it reviews what is left, then commits with trailers it can trace");
}
pub fn mismatch(gate: &str, detail: &str) {
eprintln!("\ngit-agent-verdict: error: {gate}: trailer contradicts the review\n {detail}");
}
pub fn stale(want: &str, have: &str) {
eprintln!(
"git-agent-verdict: error: {have} is older than the required {want}: cargo install git-agent-verdict --version '^{want}'"
);
}
pub fn incompatible(want: &str, have: &str) {
eprintln!(
"git-agent-verdict: error: {have} is not the {want} line this hook declares its gates against: cargo install git-agent-verdict --version '^{want}'"
);
}
pub fn malformed(gate: &str, detail: &str) {
eprintln!("\ngit-agent-verdict: error: {gate}: malformed trailer\n {detail}");
}
fn log_path(gate: &str) -> Result<std::path::PathBuf, String> {
let root = git::toplevel()?;
let name = std::path::Path::new(&root)
.file_name()
.map_or_else(|| "repo".to_string(), |n| n.to_string_lossy().into_owned());
let slug = format!("{name}-{}", &state::fingerprint(&root)[..8]);
let home = std::env::var("HOME").map_err(|_| "no HOME to write the review log under")?;
let dir = std::path::Path::new(&home)
.join(".agent-verdicts")
.join(slug);
std::fs::create_dir_all(&dir).map_err(|e| format!("cannot create {}: {e}", dir.display()))?;
let head = git::head_sha();
let mut n = 1;
while dir.join(format!("{head}-{n}-{gate}.log")).exists() {
n += 1;
}
Ok(dir.join(format!("{head}-{n}-{gate}.log")))
}
pub fn logged(gate: &str, findings: &str) -> Option<std::path::PathBuf> {
let path = log_path(gate).ok()?;
std::fs::write(&path, findings).ok()?;
Some(path)
}
pub fn maintenance(files: &[String]) {
eprintln!(
"git-agent-verdict: error: {} can never be attested — part of the rubric and infra this tool scores by.",
files.join(", ")
);
eprintln!("Review manually and commit with --no-verify.");
}
pub fn judging() {
eprintln!("git-agent-verdict: judging the intent…");
}
pub enum Standing {
Passed(String),
Blocked(String),
Waiting,
Skipped(String),
}
pub fn gates(standings: &[(String, Standing)]) {
let width = standings.iter().map(|(g, _)| g.len()).max().unwrap_or(0);
eprintln!("\nagent-verdict gates mandated by repo:");
for (gate, standing) in standings {
let said = match standing {
Standing::Passed(counts) => format!("PASSED - {counts}"),
Standing::Blocked(counts) => format!("BLOCKED - {counts}, fix and attest again"),
Standing::Waiting => "PENDING".to_string(),
Standing::Skipped(paths) => format!("SKIPPED - nothing staged matches {paths}"),
};
eprintln!(" {gate:width$} [{said}]");
}
eprintln!();
}
pub fn reviewed(
gate: &str,
verdicts: &[Verdict],
blocked: bool,
next: Option<&str>,
findings: &str,
standings: &[(String, Standing)],
) {
println!("{gate}: {}", summarize(verdicts));
if !findings.is_empty() {
match logged(gate, findings) {
Some(path) => eprintln!("\nsee the full report: {}", path.display()),
None => eprintln!("\n{findings}"),
}
}
gates(standings);
if blocked {
eprintln!("git-agent-verdict: error: MAJOR — gate not passed. Fix what the review named, then attest again.");
return;
}
match next {
Some(gate) => {
eprintln!("next: address the findings, then attest again for {gate}");
}
None => eprintln!("next: attest again — every gate is through, so that run commits"),
}
}
pub fn committed(trailers: &[String], out: &str) {
println!(
"committed {} — every gate attested, nothing left to run",
git::head_sha()
);
print!("{out}");
if trailers.is_empty() {
eprintln!(" no verdict: no gate read this commit — see above for which, and why");
return;
}
for line in trailers {
eprintln!(" {line}");
}
}
pub fn reset_done(count: u32, reason: &str) {
eprintln!("git-agent-verdict: review state cleared (reset {count} for this HEAD): {reason}");
eprintln!(" the reason is recorded and reaches the commit message");
}