use crate::declarations::{self, Declaration, Hook};
use crate::gate;
use crate::git;
use crate::report;
use crate::standing::{applies, latest, next, settled, survey};
use crate::state;
use crate::trailer::{self, Verdict};
fn session_of(step: &state::Step) -> Option<String> {
let record = state::lookup(&step.token).ok()??;
Some(record.verdicts.first()?.session.clone()).filter(|s| !s.is_empty())
}
fn prior_session(declaration: &Declaration, steps: &[state::Step]) -> Option<String> {
session_of(latest(&declaration.gate, steps)?)
}
enum Opening {
First,
Again,
Interrupted,
}
struct Round {
opening: Opening,
session: crate::agent::Session,
}
fn round_for(declaration: &Declaration, steps: &[state::Step]) -> Result<Round, String> {
if let Some(held) = state::in_flight()? {
if held.gate == declaration.gate && crate::agent::transcript(&held.session).is_some() {
return Ok(Round {
opening: Opening::Interrupted,
session: crate::agent::Session::resumed(&held.session),
});
}
}
Ok(match prior_session(declaration, steps) {
Some(session) => Round {
opening: Opening::Again,
session: crate::agent::Session::resumed(&session),
},
None => Round {
opening: Opening::First,
session: crate::agent::Session::opened(),
},
})
}
fn briefing(
declaration: &Declaration,
intent: &str,
round: &Round,
) -> Result<(String, String), String> {
let system = crate::brief::system(declaration, crate::brief::Reach::Diff)?;
let prompt = match round.opening {
Opening::First => crate::brief::opening(intent),
Opening::Again => crate::brief::continuing(),
Opening::Interrupted => crate::brief::resuming(),
};
Ok((system, prompt))
}
fn review(
declaration: &Declaration,
agent: &crate::agent::Agent,
(system, prompt): (&str, &str),
round: &Round,
ceiling: std::time::Duration,
) -> Result<(Vec<Verdict>, String), String> {
let answer = agent
.run(
crate::agent::Role::Review,
system,
prompt,
&round.session,
&crate::agent::Terms {
model: declaration.model.as_deref(),
ceiling,
read_only: declaration.read_only,
},
)
.map_err(|said| declared_model_fault(declaration, &said))?;
let verdicts = crate::runner::verdicts(&answer, declaration.brief.simple)?;
Ok((verdicts, crate::runner::findings(&answer.text)))
}
fn declared_model_fault(declaration: &Declaration, said: &str) -> String {
let Some(model) = &declaration.model else {
return said.to_string();
};
let hook = git::hook_path().unwrap_or_else(|_| "the commit-msg hook".to_string());
format!(
"gate '{}' declares --model {model}, and the agent answered:\n {said}\nThe declaration is in {hook}. Changing it is maintenance, committed with --no-verify.",
declaration.gate
)
}
fn trailers(hook: &Hook, steps: &[state::Step]) -> Result<Vec<String>, String> {
let resets = state::resets()?;
let mut lines = Vec::new();
for declaration in &hook.gates {
if !settled(declaration, steps) || !applies(declaration)? {
continue;
}
let Some(step) = latest(&declaration.gate, steps) else {
continue;
};
let Some(record) = state::lookup(&step.token)? else {
continue;
};
for verdict in record.verdicts {
let verdict = Verdict { resets, ..verdict };
lines.push(trailer::render(&record.gate, &verdict));
}
}
if lines.is_empty() {
if git::staged(&[])?.is_empty() {
return Err("nothing staged: nothing to review, nothing to commit".to_string());
}
return Err(format!(
"{} declares no gate matching this commit",
hook.path
));
}
Ok(lines)
}
fn compose(intent: &str, trailers: &[String], resets: &[String]) -> String {
let mut message = format!("{intent}\n");
for reason in resets {
message.push_str(&format!("\nReset: {reason}\n"));
}
if trailers.is_empty() {
return message;
}
message.push('\n');
message.push_str(&trailers.join("\n"));
message.push('\n');
message
}
fn land(hook: &Hook, steps: &[state::Step], intent: Option<&str>) -> Result<bool, String> {
let trailers = trailers(hook, steps)?;
let intent = intent.ok_or("this commit has no accepted intent")?;
let message = compose(intent, &trailers, &state::reasons()?);
let out = git::commit(&message)?;
report::committed(&trailers, &out);
Ok(true)
}
pub fn run(asked: Option<&str>, ceiling: std::time::Duration) -> Result<bool, String> {
let hook = declarations::read()?;
let mut drifting: Vec<String> = Vec::new();
for declaration in &hook.gates {
for file in git::unstaged(&declaration.paths)? {
if !drifting.contains(&file) {
drifting.push(file);
}
}
}
if !drifting.is_empty() {
report::drifted(&drifting);
return Ok(false);
}
let staged_machinery = gate::machinery_staged()?;
if !staged_machinery.is_empty() {
report::maintenance(&staged_machinery);
return Ok(false);
}
crate::runner::configured()?;
let held = crate::lock::take()?;
let steps = state::progress()?;
let recorded = state::intent()?;
let proposed = state::proposed()?;
match (asked, recorded.as_deref(), proposed.as_deref()) {
(Some(_), Some(held), _) => {
return Err(format!(
"this commit already has an accepted intent, which is fixed:\n {held}\nattest takes no --intent after the first run."
))
}
(Some(asked), None, Some(standing)) if standing != asked => {
return Err(format!(
"this commit already has a proposed intent, which is fixed:\n {standing}\nRun attest with no --intent, or reset to state another."
))
}
(Some(asked), None, _) => state::propose(asked)?,
(None, None, None) => {
return Err("attest needs --intent: no intent is recorded for this commit".to_string())
}
_ => {}
}
if next(&hook, &steps)?.is_none() {
if git::staged(&[])?.is_empty() {
return Err("nothing staged: nothing to review, nothing to commit".to_string());
}
if !survey(&hook, &steps)?
.iter()
.any(|(_, standing)| !matches!(standing, report::Standing::Skipped(_)))
{
return Err(format!(
"{} declares no gate matching this commit",
hook.path
));
}
report::what_was_reviewed();
report::all_passed();
return Ok(true);
}
let started = crate::round::spawn(held, "attest", ceiling, move |round| {
review_all(&hook, round, ceiling)
})?;
report::started(&started);
Ok(true)
}
pub fn commit() -> Result<bool, String> {
let hook = declarations::read()?;
let held = crate::lock::take()?;
let steps = state::progress()?;
if let Some(declaration) = next(&hook, &steps)? {
report::what_was_reviewed();
return Err(report::not_passed(&declaration.gate));
}
held.describe(&crate::lock::Landed::Landing)?;
let landed = land(&hook, &steps, state::intent()?.as_deref());
if landed.is_ok() {
crate::round::forget_last();
}
landed
}
pub fn abandon() -> Result<bool, String> {
crate::round::abort(|| {
state::close_round();
let _ = state::settle_intent(false);
declarations::read()
.and_then(|hook| survey(&hook, &state::progress()?))
.unwrap_or_default()
})
}
fn review_all(
hook: &Hook,
round: &crate::round::Round,
ceiling: std::time::Duration,
) -> Result<crate::round::Outcome, String> {
let intent = accepted_intent(ceiling)?;
loop {
let steps = state::progress()?;
let Some(declaration) = next(hook, &steps)? else {
return Ok(crate::round::Outcome::Clean);
};
if review_one(hook, round, declaration, &steps, &intent, ceiling)? {
return Ok(crate::round::Outcome::Blocked);
}
}
}
fn accepted_intent(ceiling: std::time::Duration) -> Result<String, String> {
let intent = match (state::intent()?, state::proposed()?) {
(Some(accepted), _) => accepted,
(None, Some(asked)) => {
let judge = crate::runner::configured()?;
report::judging();
let answer = judge.run(
crate::agent::Role::JudgeIntent,
&crate::brief::judge_system(),
&crate::brief::judge_prompt(&asked),
&crate::agent::Session::opened(),
&crate::agent::Terms {
model: None,
ceiling,
read_only: false,
},
)?;
let judged = crate::runner::judge(&answer, &asked);
state::settle_intent(judged.is_ok())?;
judged?;
asked
}
(None, None) => {
return Err("no intent was recorded for this review to validate".to_string())
}
};
Ok(intent)
}
fn review_one(
hook: &Hook,
round: &crate::round::Round,
declaration: &Declaration,
steps: &[state::Step],
intent: &str,
ceiling: std::time::Duration,
) -> Result<bool, String> {
let agent = crate::runner::configured()?;
round.at_gate(&declaration.gate);
let opened = round_for(declaration, steps)?;
if matches!(opened.opening, Opening::Interrupted) {
report::resuming(
&declaration.gate,
opened.session.id(),
crate::agent::last_wrote(opened.session.id()),
);
}
let (system, prompt) = briefing(declaration, intent, &opened)?;
state::open_round(&declaration.gate, opened.session.id())?;
report::reviewing(
&declaration.gate,
opened.session.id(),
crate::agent::transcript_path(opened.session.id()).as_deref(),
);
crate::signals::say(&format!(
"while reviewing {}, session {}.",
declaration.gate,
opened.session.id()
));
let reviewed = review(declaration, &agent, (&system, &prompt), &opened, ceiling);
crate::signals::quiet();
if reviewed.is_err() && matches!(opened.opening, Opening::Interrupted) {
state::close_round();
}
let (verdicts, findings) = reviewed?;
let blocked = verdicts.iter().any(Verdict::blocks);
state::record(&declaration.gate, &verdicts, blocked)?;
let after = state::progress()?;
let remaining = next(hook, &after)?.map(|d| d.gate.clone());
report::reviewed(
round.dir(),
&declaration.gate,
&verdicts,
blocked,
remaining.as_deref(),
&findings,
&survey(hook, &after)?,
);
Ok(blocked)
}
pub fn reset(reason: &str) -> Result<bool, String> {
let count = state::log_reset(reason)?;
crate::round::abandon_logs();
report::reset_done(count, reason);
Ok(true)
}