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>")
}
fn here() -> String {
git::toplevel().unwrap_or_else(|_| "<abs path to the repo root>".to_string())
}
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 --repo {} --intent \"<the aim, one flat line>\"\n",
here()
);
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 --repo {} --intent \"…\"",
here()
);
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 drifted(files: &[String]) {
eprintln!(
"git-agent-verdict: error: the index and the working tree disagree on {}.",
files.join(", ")
);
eprintln!("The reviewer opens the files; the commit carries the index. Stage or restore them.");
}
pub fn judging() {
eprintln!("git-agent-verdict: judging the intent…");
}
pub fn abandoned(pid: &str, path: &std::path::Path, ago: u64) {
eprintln!(
"git-agent-verdict: removing a stale lock file: {}",
path.display()
);
eprintln!(" attest pid {pid} wrote it {ago}s ago and that process is gone, so it was killed or it crashed");
eprintln!(" the file records when it was written, not when that run stopped — nothing here dates the end");
}
const LATEST: &str = r#"jq -rc 'select(.type=="assistant") | .timestamp[11:19] as $t | .message.content[] | if .type=="tool_use" then "\($t) \(.name) \(.input|tostring|gsub("\n";" "))" elif .type=="text" then "\($t) » \(.text|gsub("\n";" "))" else empty end'"#;
pub fn reviewing(gate: &str, session: &str, transcript: Option<&std::path::Path>) {
eprintln!(
"git-agent-verdict: {gate}: reviewing — session {session}, pid {}",
std::process::id()
);
let Some(path) = transcript else {
return;
};
println!("progress log being appended here: {}", path.display());
println!(
" latest activity: {LATEST} {} | cut -c1-110 | tail -5",
path.display()
);
}
pub fn still_reviewing(elapsed: u64, ceiling: u64) {
eprintln!("git-agent-verdict: still reviewing — {elapsed}s of {ceiling}s");
}
pub fn resuming(gate: &str, session: &str, quiet_for: Option<u64>) {
eprintln!("git-agent-verdict: {gate}: the last run opened a review and never recorded one, so it died mid-review");
eprintln!(
" resuming that same reviewer rather than paying for the gate again — session {session}"
);
if let Some(seconds) = quiet_for {
eprintln!(" its transcript was last written {seconds}s ago, which is the closest thing to a time of death recorded anywhere");
}
}
pub fn auditing(hook: &str) {
eprintln!(
"git-agent-verdict: auditing the whole repository against every gate {hook} declares"
);
eprintln!(" one review per gate, on the tree as it stands. Nothing is recorded and nothing is committed.");
}
pub fn audited(gate: &str, verdicts: &[Verdict], findings: &str) {
println!("{gate}: {}", summarize(verdicts));
if findings.is_empty() {
return;
}
match logged(gate, findings) {
Some(path) => eprintln!(" the full report: {}", path.display()),
None => eprintln!("\n{findings}"),
}
}
pub fn gate_failed(gate: &str, detail: &str) {
eprintln!("git-agent-verdict: error: {gate}: no verdict — {detail}");
}
pub fn audit_done(gates: usize, blocked: bool, failed: &[String]) {
let rung = if blocked {
"including MAJOR"
} else {
"no MAJOR"
};
eprintln!("\ngit-agent-verdict: audited {gates} gate(s), {rung}.");
if !failed.is_empty() {
eprintln!(" no verdict from: {}", failed.join(", "));
}
eprintln!("next: fix what the reports name, in commits attested from their own diffs");
}
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");
}