use std::collections::BTreeMap;
use serde::Serialize;
use crate::config::{MergeMode, MergeStyle};
use crate::run::{
CommandOutcome, ContinuationOutcome, E2eStatus, GateStatus, JobStatus, Liveness,
OperatorFixOutcome, ReviewRound, RunState, tail,
};
use crate::verdict::{Finding, ReviewVote, Severity};
pub const SCHEMA: u32 = 1;
const OUTPUT_TAIL: usize = 2_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Tone {
Ok,
Warn,
Fail,
Neutral,
}
#[derive(Debug, Serialize)]
pub struct RunReportView {
pub schema: u32,
pub header: Header,
pub sections: Vec<Section>,
pub active_seats: String,
}
#[derive(Debug, Serialize)]
pub struct Header {
pub id: String,
pub status: String,
pub tone: Tone,
pub unmerged_by_design: bool,
pub needs_attention: bool,
pub repo: String,
pub base_branch: String,
pub base_commit: String,
pub created: String,
pub task: String,
pub origin: String,
pub state_dir: String,
}
#[derive(Debug, Serialize)]
pub struct Head {
pub title: String,
pub tone: Tone,
pub default_open: bool,
}
fn head(title: &str, tone: Tone, default_open: bool) -> Head {
Head {
title: title.to_owned(),
tone,
default_open,
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Section {
Candidates {
#[serde(flatten)]
head: Head,
items: Vec<CandidateRow>,
},
Judging {
#[serde(flatten)]
head: Head,
judges: Vec<JudgeRow>,
deliberation: Vec<String>,
rounds: Vec<DeliberationRoundView>,
votes: Vec<VoteRow>,
},
Tally {
#[serde(flatten)]
head: Head,
tally: TallyView,
},
Notes {
#[serde(flatten)]
head: Head,
key: &'static str,
lines: Vec<String>,
},
Graft {
#[serde(flatten)]
head: Head,
graft: GraftView,
},
Review {
#[serde(flatten)]
head: Head,
rounds: Vec<RoundView>,
handed_off: Option<String>,
},
OperatorFixes {
#[serde(flatten)]
head: Head,
requests: Vec<OperatorFixView>,
},
BaseSync {
#[serde(flatten)]
head: Head,
base_sync: BaseSyncView,
},
PreGate {
#[serde(flatten)]
head: Head,
commands: Vec<CommandView>,
committed: Option<String>,
},
Gate {
#[serde(flatten)]
head: Head,
status: &'static str,
commands: Vec<CommandView>,
},
Merge {
#[serde(flatten)]
head: Head,
merge: MergeView,
},
Followups {
#[serde(flatten)]
head: Head,
items: Vec<FollowupRow>,
},
ReleaseBump {
#[serde(flatten)]
head: Head,
bump: ReleaseBumpView,
},
WinnerWorktree {
#[serde(flatten)]
head: Head,
worktree: String,
branch: String,
provisional: bool,
},
Jobs {
#[serde(flatten)]
head: Head,
groups: Vec<JobGroup>,
coverage: &'static str,
empty_note: Option<&'static str>,
},
}
#[derive(Debug, Serialize)]
pub struct CandidateRow {
pub label: String,
pub agent: String,
pub state: &'static str,
pub detail: Option<String>,
pub files: usize,
pub commits: usize,
pub duration_secs: u64,
pub winner: bool,
pub provisional_winner: bool,
}
#[derive(Debug, Serialize)]
pub struct JudgeRow {
pub judge: usize,
pub agent: String,
pub ranking: String,
pub confidence: Option<u8>,
pub failed: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct VoteRow {
pub judge: usize,
pub agent: String,
pub vote: String,
pub changed: bool,
}
#[derive(Debug, Serialize)]
pub struct DeliberationTurnView {
pub judge: usize,
pub agent: String,
pub tentative: Option<String>,
pub body: String,
pub body_md: Vec<crate::md::Node>,
}
#[derive(Debug, Serialize)]
pub struct DeliberationRoundView {
pub round: usize,
pub turns: Vec<DeliberationTurnView>,
}
#[derive(Debug, Serialize)]
pub struct TallyView {
pub met_quorum: bool,
pub decided: bool,
pub provisional: bool,
pub winner: String,
pub judges: usize,
pub present: usize,
pub quorum: usize,
pub uncontested: Option<String>,
pub first_choice: BTreeMap<String, usize>,
pub initial: &'static str,
pub unanimous_final: bool,
pub changed_votes: usize,
pub tie_break: Option<String>,
pub rate_limited: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct RoundView {
pub round: usize,
pub status: &'static str,
pub tone: Tone,
pub head: String,
pub verified_head: Option<String>,
pub raised: usize,
pub blocking: usize,
pub answered: usize,
pub expected: usize,
pub missing: Vec<String>,
pub e2e: E2eView,
pub verdict: Option<VerdictView>,
pub fix: Option<FixView>,
pub commands: Vec<CommandView>,
pub reviewers: Vec<ReviewerRow>,
pub findings: Vec<FindingView>,
pub reconsideration: Vec<RevoteRow>,
pub default_open: bool,
}
#[derive(Debug, Serialize)]
pub struct E2eView {
pub state: &'static str,
pub build_failed: bool,
pub defer_reason: Option<String>,
pub retried: bool,
}
#[derive(Debug, Serialize)]
pub struct VerdictView {
pub vote: &'static str,
pub label: &'static str,
pub split: bool,
}
#[derive(Debug, Serialize)]
pub struct FixView {
pub agent: String,
pub addressed: usize,
pub rejected: usize,
pub committed: bool,
pub tree_changed: bool,
pub report_lost: Option<String>,
pub continuation: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ReviewerRow {
pub reviewer: usize,
pub agent: String,
pub vote: Option<&'static str>,
pub failed: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct RevoteRow {
pub reviewer: usize,
pub vote: Option<&'static str>,
pub reason: String,
}
#[derive(Debug, Serialize)]
pub struct FindingView {
pub id: String,
pub reviewer: Option<usize>,
pub severity: Option<&'static str>,
pub title: String,
pub file: Option<String>,
pub line: Option<u32>,
pub blocking: bool,
pub state: &'static str,
pub declined_why: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CommandView {
pub command: String,
pub state: &'static str,
pub code: Option<i32>,
pub duration_ms: u64,
pub output_tail: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct OperatorFixView {
pub requested_at: String,
pub stale: bool,
pub reason: String,
pub findings: Vec<OperatorFindingRow>,
pub follow_up_review_run: Option<String>,
pub unverified_commit: bool,
}
#[derive(Debug, Serialize)]
pub struct OperatorFindingRow {
pub id: String,
pub severity: &'static str,
pub title: String,
pub outcome: &'static str,
pub why: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct BaseSyncView {
pub base_branch: String,
pub tip: String,
pub state: &'static str,
pub behind: usize,
pub attempts: usize,
pub conflict: Option<String>,
pub already_in: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct MergeView {
pub mode: String,
pub state: &'static str,
pub detail: String,
pub branch: Option<String>,
pub squash_caveat: bool,
pub red_at_merge: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct FollowupRow {
pub task: String,
pub findings: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ReleaseBumpView {
pub version: Option<String>,
pub pr_url: Option<String>,
pub automerge: &'static str,
pub problem: Option<String>,
pub action_required: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct JobGroup {
pub node: String,
pub seat: String,
pub jobs: Vec<JobRow>,
}
#[derive(Debug, Serialize)]
pub struct JobRow {
pub id: String,
pub status: &'static str,
pub exit_code: Option<i32>,
pub round: Option<usize>,
pub checked_at: String,
pub description: String,
pub result_summary: String,
}
fn short(commit: &str) -> String {
commit.chars().take(7).collect()
}
fn first_line(text: &str) -> String {
text.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("")
.to_owned()
}
fn severity_name(s: Severity) -> &'static str {
match s {
Severity::Nit => "nit",
Severity::Minor => "minor",
Severity::Major => "major",
Severity::Blocker => "blocker",
}
}
fn vote_name(v: ReviewVote) -> &'static str {
match v {
ReviewVote::Approve => "approve",
ReviewVote::ApproveWithFindings => "approve_with_findings",
ReviewVote::Reject => "reject",
}
}
fn command(o: &CommandOutcome, warn_instead_of_fail: bool) -> CommandView {
let state = if o.resource_blocked {
"blocked"
} else if o.ok() {
"pass"
} else if warn_instead_of_fail {
"warn"
} else {
"fail"
};
CommandView {
command: o.command.clone(),
state,
code: o.code,
duration_ms: o.duration_ms,
output_tail: (!o.ok()).then(|| tail(&o.output_tail, OUTPUT_TAIL)),
}
}
fn status_tone(state: &RunState) -> Tone {
use crate::run::RunStatus as S;
if state.needs_attention() {
return Tone::Warn;
}
match state.status {
S::Merged | S::Ready => Tone::Ok,
S::Failed => Tone::Fail,
S::Stalled | S::Blocked => Tone::Warn,
_ => Tone::Neutral,
}
}
fn round_view(r: &ReviewRound, last: bool) -> RoundView {
let raised = r.reviews.iter().map(|x| x.findings.len()).sum();
let status = if r.incomplete() {
"incomplete"
} else if r.clean {
"clean"
} else {
"open"
};
let e2e_state = match r.e2e_status() {
E2eStatus::NotConfigured => "not_configured",
E2eStatus::Deferred => "deferred",
E2eStatus::Passed => "pass",
E2eStatus::Failed => "fail",
E2eStatus::ResourceBlocked => "blocked",
};
let e2e = E2eView {
state: e2e_state,
build_failed: r.e2e_status() == E2eStatus::Failed
&& r.e2e.iter().any(CommandOutcome::build_failed),
defer_reason: r
.e2e_defer_reason
.clone()
.filter(|_| e2e_state == "deferred"),
retried: r.verify_retried,
};
let tone = if e2e_state == "fail" && !e2e.build_failed {
Tone::Fail
} else if status == "clean" {
Tone::Ok
} else {
Tone::Warn
};
let missing = if r.incomplete() {
r.reviews
.iter()
.filter_map(|x| {
x.failed
.as_ref()
.map(|why| format!("review-{}: {why}", x.reviewer))
})
.collect()
} else {
Vec::new()
};
let fix = r.fix.as_ref().map(|f| FixView {
agent: f.agent.clone(),
addressed: f.addressed.len(),
rejected: f.rejected.len(),
committed: f.committed,
tree_changed: r.progressed,
report_lost: f.failed.clone(),
continuation: f.continuation.as_ref().and_then(|c| match c.outcome {
ContinuationOutcome::NotNeeded => None,
ContinuationOutcome::Resumed => Some(format!("resumed x{}", c.attempts)),
ContinuationOutcome::Exhausted => Some(format!("exhausted x{}", c.attempts)),
ContinuationOutcome::QuotaLost => Some("quota".to_owned()),
ContinuationOutcome::NoSession => Some("no session to resume".to_owned()),
}),
});
let mut findings: Vec<FindingView> = Vec::new();
for rec in &r.reviews {
for f in &rec.findings {
findings.push(finding_view(r, rec.reviewer, f));
}
}
if let Some(fix) = &r.fix {
for rej in &fix.rejected {
if !findings.iter().any(|f| f.id == rej.id) {
findings.push(FindingView {
id: rej.id.clone(),
reviewer: None,
severity: None,
title: String::new(),
file: None,
line: None,
blocking: false,
state: "declined",
declined_why: Some(rej.why.clone()),
});
}
}
}
RoundView {
round: r.round,
status,
tone,
head: short(&r.head),
verified_head: r.verified_head.as_deref().map(short),
raised,
blocking: r.blocking,
answered: r.answered,
expected: r.expected,
missing,
e2e,
verdict: r.verdict.map(|v| VerdictView {
vote: vote_name(v),
label: v.label(),
split: r.vote_split,
}),
fix,
commands: r.e2e.iter().map(|o| command(o, false)).collect(),
reviewers: r
.reviews
.iter()
.map(|x| ReviewerRow {
reviewer: x.reviewer,
agent: x.agent.clone(),
vote: x.vote.map(vote_name),
failed: x.failed.clone(),
})
.collect(),
findings,
reconsideration: r
.reconsideration
.iter()
.map(|rv| RevoteRow {
reviewer: rv.reviewer,
vote: rv.vote.map(vote_name),
reason: match rv.vote {
Some(_) => rv.reason.clone(),
None => format!("no revote ({})", rv.failed.as_deref().unwrap_or("unknown")),
},
})
.collect(),
default_open: last || status != "clean",
}
}
fn finding_view(r: &ReviewRound, reviewer: usize, f: &Finding) -> FindingView {
let fixed = r
.fix
.as_ref()
.is_some_and(|fix| fix.addressed.contains(&f.id));
let declined = r
.fix
.as_ref()
.and_then(|fix| fix.rejected.iter().find(|x| x.id == f.id));
let (state, declined_why) = match (fixed, declined) {
(true, _) => ("fixed", None),
(false, Some(rej)) => ("declined", Some(rej.why.clone())),
(false, None) => ("open", None),
};
FindingView {
id: f.id.clone(),
reviewer: Some(reviewer),
severity: Some(severity_name(f.severity)),
title: f.title.clone(),
file: f.file.clone(),
line: f.line,
blocking: f.severity.blocks(),
state,
declined_why,
}
}
fn jobs_section(state: &RunState) -> Option<Section> {
let codex = state
.config
.agents
.iter()
.any(|a| a.kind == crate::config::AgentKind::Codex);
if state.jobs.is_empty() && !codex {
return None;
}
let mut by_seat: BTreeMap<(&str, &str), Vec<&crate::run::JobRecord>> = BTreeMap::new();
for j in &state.jobs {
by_seat
.entry((j.node.as_str(), j.seat.as_str()))
.or_default()
.push(j);
}
let groups = by_seat
.into_iter()
.map(|((node, seat), records)| JobGroup {
node: node.to_owned(),
seat: seat.to_owned(),
jobs: records
.into_iter()
.map(|j| JobRow {
id: j.id.clone(),
status: match j.status {
JobStatus::Completed => "completed",
JobStatus::Failed => "failed",
JobStatus::Unknown => "unknown",
},
exit_code: j.exit_code,
round: j.round,
checked_at: j.checked_at.to_string(),
description: first_line(&j.description),
result_summary: tail(&j.result_summary, OUTPUT_TAIL),
})
.collect(),
})
.collect();
Some(Section::Jobs {
head: head("background jobs", Tone::Neutral, false),
groups,
coverage: "adapter coverage: codex only today; other backends, and a command a CLI \
never reported finishing, leave no entry here - that is unknown, never \
\"nothing ran\"",
empty_note: state.jobs.is_empty().then_some(
"no completed command evidence yet for this run (see active seats for what is \
still mid-turn)",
),
})
}
#[derive(Debug, Serialize)]
pub struct GraftStrengthView {
pub id: String,
pub source: String,
pub text: String,
pub state: &'static str,
pub why: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct GraftHandoverView {
pub from: String,
pub to: String,
pub reason: String,
}
#[derive(Debug, Serialize)]
pub struct GraftView {
pub outcome: &'static str,
pub reason: Option<String>,
pub agent: Option<String>,
pub handovers: Vec<GraftHandoverView>,
pub winner: Option<String>,
pub sources: Vec<String>,
pub lines: Option<usize>,
pub limit: Option<usize>,
pub requested: Vec<GraftStrengthView>,
pub adopted_count: usize,
pub rejected_count: usize,
pub unconfirmed_count: usize,
pub notes_md: Vec<crate::md::Node>,
pub reasoning_md: Vec<crate::md::Node>,
pub recovered: bool,
pub unavailable: Vec<crate::graft_report::Unavailable>,
pub diff_available: bool,
pub pre: String,
pub post: Option<String>,
}
fn graft_view(
state: &RunState,
g: &crate::run::GraftRecord,
rec: Option<&crate::graft_report::Recovered>,
) -> GraftView {
use crate::graft_report::{Standing, Unavailable, standing};
use crate::run::GraftOutcome;
let mut unavailable: Vec<Unavailable> = Vec::new();
let mut recovered = false;
let requested_src: Vec<_> = if g.requested.is_empty() {
match rec {
Some(r) => {
recovered |= !r.requested.is_empty();
r.requested.clone()
}
None => Vec::new(),
}
} else {
g.requested.clone()
};
let adoption = match (&g.adoption, rec) {
(Some(a), _) => Some(a.clone()),
(None, Some(r)) if g.adoption_error.is_none() => {
recovered |= r.adoption.is_some();
r.adoption.clone()
}
_ => None,
};
if let Some(r) = rec {
unavailable.extend(r.unavailable.iter().cloned());
}
if let Some(e) = &g.adoption_error {
unavailable.push(Unavailable {
what: "adoption",
reason: e.clone(),
});
}
if g.requested.is_empty() && requested_src.is_empty() && rec.is_none() {
unavailable.push(Unavailable {
what: "requested",
reason: "legacy_record".to_owned(),
});
}
if adoption.is_none() && g.adoption_error.is_none() && rec.is_none() {
unavailable.push(Unavailable {
what: "adoption",
reason: "legacy_record".to_owned(),
});
}
let requested: Vec<GraftStrengthView> = requested_src
.iter()
.map(|s| {
let (st, why) = match adoption.as_ref().map(|a| standing(s, a)) {
None => ("unknown", None),
Some(Standing::Adopted) => ("adopted", None),
Some(Standing::Rejected(w)) => ("rejected", Some(w).filter(|w| !w.is_empty())),
Some(Standing::Unreported) => ("unreported", None),
};
GraftStrengthView {
id: s.id.clone(),
source: s.source.to_string(),
text: s.text.clone(),
state: st,
why,
}
})
.collect();
let count = |st: &str| requested.iter().filter(|r| r.state == st).count();
let (adopted_count, rejected_count) = (count("adopted"), count("rejected"));
let mut sources: Vec<String> = requested.iter().map(|r| r.source.clone()).collect();
sources.dedup();
let (outcome, reason) = match &g.outcome {
GraftOutcome::Applied => ("applied", None),
GraftOutcome::Running => ("unfinished", None),
GraftOutcome::RolledBack(w) => ("rolled_back", Some(w.clone())),
};
let md = |t: &str| {
if t.trim().is_empty() {
Vec::new()
} else {
crate::md::to_nodes(t, &crate::md::ImageBase::None)
}
};
let short = |c: &str| c.chars().take(12).collect::<String>();
if outcome == "applied" && g.post.is_none() {
unavailable.push(Unavailable {
what: "diff",
reason: "no_post_commit".to_owned(),
});
}
GraftView {
outcome,
reason,
agent: g.agent.clone(),
handovers: state
.handovers
.iter()
.filter(|h| h.node == "graft")
.map(|h| GraftHandoverView {
from: h.from.clone(),
to: h.to.clone(),
reason: h.reason.clone(),
})
.collect(),
winner: state.tally.as_ref().map(|t| t.winner.to_string()),
sources,
lines: g.lines,
limit: g.limit,
unconfirmed_count: requested.len() - adopted_count - rejected_count,
adopted_count,
rejected_count,
requested,
notes_md: adoption.as_ref().map(|a| md(&a.notes)).unwrap_or_default(),
reasoning_md: adoption
.as_ref()
.map(|a| md(&a.reasoning))
.unwrap_or_default(),
recovered,
unavailable,
diff_available: g.post.is_some(),
pre: short(&g.pre),
post: g.post.as_deref().map(short),
}
}
pub fn build(state: &RunState, live: Liveness) -> RunReportView {
build_with(state, live, None)
}
pub fn build_with(
state: &RunState,
live: Liveness,
graft: Option<&crate::graft_report::Recovered>,
) -> RunReportView {
let mut sections: Vec<Section> = Vec::new();
let met_quorum = state.tally.as_ref().is_none_or(|t| t.met_quorum);
sections.push(Section::Candidates {
head: head(
"candidates",
if state.candidates.iter().any(|c| c.failed.is_some()) {
Tone::Warn
} else {
Tone::Neutral
},
true,
),
items: state
.candidates
.iter()
.map(|c| {
let is_winner = state.tally.as_ref().is_some_and(|t| t.winner == c.label);
let (st, detail) = match (&c.failed, c.empty, &c.verified_noop) {
(Some(e), _, _) => ("failed", Some(e.clone())),
(None, true, Some(ev)) => ("verified_noop", Some(first_line(ev))),
(None, true, None) => ("no_change", None),
_ => (
"changed",
Some(first_line(&c.summary)).filter(|s| !s.is_empty()),
),
};
CandidateRow {
label: c.label.to_string(),
agent: c.agent.clone(),
state: st,
detail,
files: c.files,
commits: c.commits,
duration_secs: c.duration_ms / 1000,
winner: is_winner && met_quorum,
provisional_winner: is_winner && !met_quorum,
}
})
.collect(),
});
if !state.judgements.is_empty() {
sections.push(Section::Judging {
head: head("blind judging", Tone::Neutral, false),
judges: state
.judgements
.iter()
.map(|j| JudgeRow {
judge: j.judge,
agent: j.agent.clone(),
ranking: j.ranking.iter().collect(),
confidence: j.confidence,
failed: j.failed.clone(),
})
.collect(),
deliberation: state
.tally
.as_ref()
.filter(|t| t.deliberated)
.map(|_| {
state
.deliberation
.iter()
.flat_map(|round| {
round.turns.iter().map(move |turn| {
format!(
"r{} judge {} -> {}",
round.round,
turn.judge,
turn.tentative.map_or("-".to_owned(), |c| c.to_string())
)
})
})
.collect()
})
.unwrap_or_default(),
rounds: state
.deliberation
.iter()
.map(|r| DeliberationRoundView {
round: r.round,
turns: r
.turns
.iter()
.map(|t| DeliberationTurnView {
judge: t.judge,
agent: t.agent.clone(),
tentative: t.tentative.map(|c| c.to_string()),
body: t.body.clone(),
body_md: crate::md::to_nodes(&t.body, &crate::md::ImageBase::None),
})
.collect(),
})
.collect(),
votes: state
.votes
.iter()
.map(|v| VoteRow {
judge: v.judge,
agent: v.agent.clone(),
vote: v.vote.unwrap_or('?').to_string(),
changed: v.changed,
})
.collect(),
});
}
if let Some(t) = &state.tally {
let initial = match (t.rankings, t.unanimous_initial) {
(0, _) => "none",
(1, _) => "one",
(_, true) => "unanimous",
(_, false) => "split",
};
sections.push(Section::Tally {
head: head(
"tally",
if t.met_quorum { Tone::Ok } else { Tone::Fail },
true,
),
tally: TallyView {
met_quorum: t.met_quorum,
decided: t.met_quorum,
provisional: !t.met_quorum,
winner: t.winner.to_string(),
judges: t.judges,
present: t.present,
quorum: t.quorum,
uncontested: t.uncontested.clone(),
first_choice: t
.first_choice
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
initial,
unanimous_final: t.unanimous_final,
changed_votes: t.changed_votes,
tie_break: t.tie_break.clone(),
rate_limited: state.quota.iter().map(|q| q.seat.clone()).collect(),
},
});
}
let notes = |key: &'static str, title: &str, tone: Tone, lines: Vec<String>| {
(!lines.is_empty()).then(|| Section::Notes {
head: head(title, tone, true),
key,
lines,
})
};
sections.extend(notes(
"handovers",
"seats handed over",
Tone::Warn,
state
.handovers
.iter()
.map(|h| {
format!(
"{} {} -> {} ({}: {})",
h.seat, h.from, h.to, h.node, h.reason
)
})
.collect(),
));
sections.extend(notes(
"withheld",
"withheld from commit",
Tone::Warn,
state
.withheld
.iter()
.map(|w| {
format!(
"{} ({} lockfile; the directory uses {})",
w.path, w.manager, w.kept_by
)
})
.collect(),
));
if let Some(g) = &state.graft {
let view = graft_view(state, g, graft);
let tone = match view.outcome {
"applied" if view.unconfirmed_count == 0 && !view.requested.is_empty() => Tone::Ok,
"applied" => Tone::Neutral,
_ => Tone::Warn,
};
sections.push(Section::Graft {
head: head("graft", tone, true),
graft: view,
});
}
if !state.reviews.is_empty() {
let n = state.reviews.len();
let rounds: Vec<RoundView> = state
.reviews
.iter()
.enumerate()
.map(|(i, r)| round_view(r, i + 1 == n))
.collect();
let handed_off = state.handed_off_with_open_findings().then(|| {
format!(
"handed off with {} finding(s) still open - gate and e2e were green; see above \
for what a person should still look at",
state.open_findings().len()
)
});
let tone = if handed_off.is_some() {
Tone::Warn
} else {
rounds.last().map_or(Tone::Neutral, |r| r.tone)
};
sections.push(Section::Review {
head: head("review + verification", tone, true),
rounds,
handed_off,
});
}
if !state.operator_fixes.is_empty() {
sections.push(Section::OperatorFixes {
head: head("operator fix(es)", Tone::Neutral, true),
requests: state
.operator_fixes
.iter()
.map(|req| OperatorFixView {
requested_at: req.requested_at.to_string(),
stale: req.stale,
reason: req.reason.clone(),
findings: req
.findings
.iter()
.map(|f| {
let (outcome, why) = match &f.outcome {
OperatorFixOutcome::Pending => ("pending", None),
OperatorFixOutcome::Addressed => ("addressed", None),
OperatorFixOutcome::Rejected { why } => {
("rejected", Some(why.clone()))
}
OperatorFixOutcome::Unreported => ("unreported", None),
};
OperatorFindingRow {
id: f.id.clone(),
severity: severity_name(f.severity),
title: f.title.clone(),
outcome,
why,
}
})
.collect(),
follow_up_review_run: req.follow_up_review_run.clone(),
unverified_commit: req.follow_up_review_run.is_none()
&& req.fix.as_ref().is_some_and(|fx| fx.committed),
})
.collect(),
});
}
if let Some(bs) = &state.base_sync {
let (st, tone) = if bs.already_in.is_some() {
("already_in", Tone::Neutral)
} else if bs.conflict.is_some() {
("conflict", Tone::Fail)
} else if bs.behind == 0 {
("in_sync", Tone::Ok)
} else {
("behind", Tone::Warn)
};
sections.push(Section::BaseSync {
head: head("base sync", tone, true),
base_sync: BaseSyncView {
base_branch: state.base_branch.clone(),
tip: short(&bs.tip),
state: st,
behind: bs.behind,
attempts: bs.attempts,
conflict: bs.conflict.as_deref().map(first_line),
already_in: bs
.already_in
.as_ref()
.map(|a| format!("{} ({} match)", a.names(), a.proof.as_str())),
},
});
}
if !state.pre_gate.is_empty() {
let all_ok = state.pre_gate.iter().all(CommandOutcome::ok);
sections.push(Section::PreGate {
head: head(
"pre_gate",
if all_ok { Tone::Ok } else { Tone::Warn },
!all_ok,
),
commands: state.pre_gate.iter().map(|o| command(o, true)).collect(),
committed: state.pre_gate_commit.as_deref().map(short),
});
}
match state.gate_status() {
GateStatus::NotRun => {}
gs => {
let st = match gs {
GateStatus::PassedWithNoCommands => "passed_no_commands",
GateStatus::Passed => "passed",
_ => "failed",
};
sections.push(Section::Gate {
head: head("gate", if gs.ok() { Tone::Ok } else { Tone::Fail }, true),
status: st,
commands: state.gate.iter().map(|o| command(o, false)).collect(),
});
}
}
if let Some(m) = &state.merge {
let by_design = m.mode == MergeMode::None;
let (st, tone) = if by_design {
("not_landed_by_design", Tone::Neutral)
} else if m.ok {
("ok", Tone::Ok)
} else if m.empty {
("empty", Tone::Warn)
} else {
("not_merged", Tone::Warn)
};
sections.push(Section::Merge {
head: head("merge", tone, true),
merge: MergeView {
mode: format!("{:?}", m.mode).to_lowercase(),
state: st,
detail: m.detail.lines().next().unwrap_or("").to_owned(),
branch: by_design
.then(|| state.winner().map(|w| w.branch.clone()))
.flatten(),
squash_caveat: by_design && state.config.merge.style == MergeStyle::Squash,
red_at_merge: state
.pr
.as_ref()
.map(|pr| pr.red_at_merge.clone())
.unwrap_or_default(),
},
});
}
if !state.followups.is_empty() {
sections.push(Section::Followups {
head: head("follow-ups", Tone::Neutral, true),
items: state
.followups
.iter()
.map(|f| FollowupRow {
task: crate::queue::short(&f.task).to_owned(),
findings: f.findings.clone(),
})
.collect(),
});
}
if let Some(b) = &state.release_bump {
let automerge = if b.automerge_enabled {
"enabled"
} else if b.merged_directly {
"merged_directly"
} else if b.local {
"local"
} else if b.problem.is_some() {
"failed"
} else {
"not_enabled"
};
sections.push(Section::ReleaseBump {
head: head(
"release bump",
if automerge == "failed" || b.action_required.is_some() {
Tone::Warn
} else {
Tone::Neutral
},
true,
),
bump: ReleaseBumpView {
version: b.version.clone(),
pr_url: b.pr_url.clone(),
automerge,
problem: b.problem.as_deref().map(first_line),
action_required: b.action_required.clone(),
},
});
}
sections.extend(notes(
"leaks",
"blindness warnings",
Tone::Warn,
state
.leaks
.iter()
.map(|l| format!("{} x{} in {}", l.token, l.count, l.site))
.collect(),
));
if let Some(w) = state.winner()
&& !w.folded
{
sections.push(Section::WinnerWorktree {
head: head("winner worktree", Tone::Neutral, true),
worktree: w.worktree.display().to_string(),
branch: w.branch.clone(),
provisional: !met_quorum,
});
}
sections.extend(jobs_section(state));
RunReportView {
schema: SCHEMA,
header: Header {
id: state.id.clone(),
status: state.status.display_label().to_owned(),
tone: status_tone(state),
unmerged_by_design: state.unmerged_by_design(),
needs_attention: state.needs_attention(),
repo: state.repo.display().to_string(),
base_branch: state.base_branch.clone(),
base_commit: short(&state.base_commit),
created: state.created_local(),
task: first_line(&state.instruction),
origin: crate::run::origin_label(state.origin.as_ref()),
state_dir: state.dir().display().to_string(),
},
sections,
active_seats: crate::report::active_seats(state, live),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::run::{CommandOutcome, FixRecord, ReviewRecord, Tally};
use crate::verdict::Rejection;
use std::path::PathBuf;
fn state() -> RunState {
crate::run::pin_test_home();
RunState::new(
PathBuf::from("/repo/magi"),
"main".to_owned(),
"0123456789abcdef".to_owned(),
"Add a thing\n\nlong".to_owned(),
Config::default(),
)
}
fn finding(id: &str, severity: Severity) -> Finding {
Finding {
id: id.to_owned(),
severity,
file: Some("src/a.rs".to_owned()),
line: Some(7),
title: format!("title {id}"),
detail: String::new(),
}
}
fn cmd(code: Option<i32>, blocked: bool) -> CommandOutcome {
CommandOutcome {
command: "cargo test".to_owned(),
code,
output_tail: "boom\n".repeat(1_000),
duration_ms: 5,
resource_blocked: blocked,
}
}
fn round(n: usize) -> ReviewRound {
let mut r: ReviewRound = serde_json::from_value(serde_json::json!({
"round": n, "head": "abcdef0123", "reviews": []
}))
.expect("round");
r.answered = 1;
r.expected = 1;
r.reviews.push(
serde_json::from_value::<ReviewRecord>(serde_json::json!({
"reviewer": 1, "agent": "a"
}))
.expect("record"),
);
r
}
fn tally(met: bool) -> Tally {
serde_json::from_value(serde_json::json!({
"first_choice": {"A": 1}, "borda": {"A": 2}, "winner": "A",
"unanimous_initial": true, "deliberated": false, "changed_votes": 0,
"unanimous_final": true, "judges": 3, "present": if met { 3 } else { 1 },
"quorum": 2, "met_quorum": met, "rankings": 1
}))
.expect("tally")
}
fn kinds(v: &RunReportView) -> Vec<String> {
serde_json::to_value(&v.sections)
.expect("json")
.as_array()
.expect("array")
.iter()
.map(|s| s["kind"].as_str().expect("kind").to_owned())
.collect()
}
#[test]
fn sections_follow_the_text_report_order() {
let mut s = state();
s.tally = Some(tally(true));
s.reviews.push(round(1));
s.base_sync = Some(
serde_json::from_value(serde_json::json!({"tip": "abc", "behind": 0, "attempts": 0}))
.expect("sync"),
);
s.pre_gate.push(cmd(Some(0), false));
s.jobs.push(
serde_json::from_value(serde_json::json!({
"node": "fix", "seat": "fix", "id": "j1", "description": "ls",
"checked_at": "2026-01-01T00:00:00Z", "status": "Completed",
"exit_code": 0, "source": "codex"
}))
.expect("job"),
);
let v = build(&s, Liveness::Unknown);
assert_eq!(
kinds(&v),
[
"candidates",
"tally",
"review",
"base_sync",
"pre_gate",
"jobs"
]
);
let json = serde_json::to_value(&v).expect("json");
assert_eq!(json["schema"], SCHEMA);
let jobs = json["sections"].as_array().expect("a").last().expect("l");
assert_eq!(jobs["default_open"], false, "jobs start collapsed");
assert_eq!(jobs["groups"][0]["jobs"][0]["status"], "completed");
}
#[test]
fn a_stalled_tally_is_never_decided() {
let mut s = state();
s.tally = Some(tally(false));
let json = serde_json::to_value(build(&s, Liveness::Unknown)).expect("json");
let t = json["sections"]
.as_array()
.expect("a")
.iter()
.find(|x| x["kind"] == "tally")
.expect("tally");
assert_eq!(t["tally"]["met_quorum"], false);
assert_eq!(t["tally"]["decided"], false);
assert_eq!(t["tally"]["provisional"], true);
assert_eq!(t["tone"], "fail");
}
#[test]
fn findings_keep_severity_blocking_and_what_the_fixer_said() {
let mut s = state();
let mut r = round(1);
r.reviews[0].findings = vec![
finding("R1-1-1", Severity::Blocker),
finding("R1-1-2", Severity::Major),
finding("R1-1-3", Severity::Minor),
finding("R1-1-4", Severity::Nit),
];
r.fix = Some(
serde_json::from_value::<FixRecord>(serde_json::json!({"agent": "f"})).expect("fix"),
);
let fix = r.fix.as_mut().expect("fix");
fix.addressed = vec!["R1-1-1".to_owned()];
fix.rejected = vec![
Rejection {
id: "R1-1-2".to_owned(),
why: "not a bug".to_owned(),
},
Rejection {
id: "R9-9-9".to_owned(),
why: "unknown id".to_owned(),
},
];
s.reviews.push(r);
let json = serde_json::to_value(build(&s, Liveness::Unknown)).expect("json");
let review = json["sections"]
.as_array()
.expect("a")
.iter()
.find(|x| x["kind"] == "review")
.expect("review");
let f = &review["rounds"][0]["findings"];
let at = |i: usize| {
(
f[i]["severity"].clone(),
f[i]["blocking"].clone(),
f[i]["state"].clone(),
)
};
assert_eq!(at(0), ("blocker".into(), true.into(), "fixed".into()));
assert_eq!(at(1), ("major".into(), true.into(), "declined".into()));
assert_eq!(f[1]["declined_why"], "not a bug");
assert_eq!(at(2), ("minor".into(), false.into(), "open".into()));
assert_eq!(at(3), ("nit".into(), false.into(), "open".into()));
assert_eq!(
f[4]["id"], "R9-9-9",
"an unmatched declined id is not dropped"
);
assert_eq!(f[4]["state"], "declined");
}
#[test]
fn a_deferred_e2e_is_not_a_failed_one() {
let mut s = state();
let mut deferred = round(1);
deferred.e2e_deferred = true;
deferred.e2e_defer_reason = Some("blocking findings".to_owned());
let mut failed = round(2);
failed.e2e = vec![cmd(Some(1), false)];
let mut blocked = round(3);
blocked.e2e = vec![cmd(None, true)];
let none = round(4);
s.reviews = vec![deferred, failed, blocked, none];
let json = serde_json::to_value(build(&s, Liveness::Unknown)).expect("json");
let rounds = &json["sections"]
.as_array()
.expect("a")
.iter()
.find(|x| x["kind"] == "review")
.expect("review")["rounds"];
let st = |i: usize| {
rounds[i]["e2e"]["state"]
.as_str()
.expect("state")
.to_owned()
};
assert_eq!(
[st(0), st(1), st(2), st(3)],
["deferred", "fail", "blocked", "not_configured"]
);
assert_eq!(rounds[0]["e2e"]["defer_reason"], "blocking findings");
let tail = rounds[1]["commands"][0]["output_tail"]
.as_str()
.expect("tail");
assert!(tail.len() < 2_100, "output is bounded: {}", tail.len());
}
fn graft_json(s: &RunState, rec: Option<&crate::graft_report::Recovered>) -> serde_json::Value {
let json = serde_json::to_value(build_with(s, Liveness::Unknown, rec)).expect("json");
json["sections"]
.as_array()
.expect("a")
.iter()
.find(|x| x["kind"] == "graft")
.cloned()
.unwrap_or(serde_json::Value::Null)
}
fn graft_record(outcome: crate::run::GraftOutcome) -> crate::run::GraftRecord {
use crate::run::{GraftAdoption, GraftRejection, GraftStrength};
crate::run::GraftRecord {
pre: "aaaaaaaaaaaaaaaa".to_owned(),
post: Some("bbbbbbbbbbbbbbbb".to_owned()),
lines: Some(25),
limit: Some(227),
agent: Some("astra".to_owned()),
outcome,
requested: vec![
GraftStrength {
id: "A1".into(),
source: 'A',
text: "Build Persona directly in the test".into(),
},
GraftStrength {
id: "A2".into(),
source: 'A',
text: "Cache the lookup".into(),
},
GraftStrength {
id: "A3".into(),
source: 'A',
text: "Rename the helper".into(),
},
],
adoption: Some(GraftAdoption {
addressed: vec!["A1".into()],
rejected: vec![GraftRejection {
id: "A2".into(),
why: "too large".into(),
}],
notes: "ported <script>alert(1)</script>".into(),
reasoning: "thinking".into(),
}),
adoption_error: None,
}
}
#[test]
fn an_applied_graft_separates_requested_from_confirmed() {
let mut s = state();
s.graft = Some(graft_record(crate::run::GraftOutcome::Applied));
s.handovers.push(crate::run::Handover {
at: jiff::Timestamp::now(),
node: "graft".into(),
seat: "graft".into(),
from: "sonnet".into(),
to: "astra".into(),
reason: "timed out".into(),
});
let g = &graft_json(&s, None)["graft"];
assert_eq!(g["outcome"], "applied");
assert_eq!(g["agent"], "astra");
assert_eq!(g["handovers"][0]["from"], "sonnet");
assert_eq!(g["lines"], 25);
assert_eq!(g["limit"], 227);
let st: Vec<_> = g["requested"]
.as_array()
.expect("a")
.iter()
.map(|r| r["state"].as_str().expect("s").to_owned())
.collect();
assert_eq!(st, ["adopted", "rejected", "unreported"]);
assert_eq!(g["requested"][1]["why"], "too large");
assert_eq!(g["adopted_count"], 1);
assert_eq!(g["unconfirmed_count"], 1);
assert_eq!(g["diff_available"], true);
}
#[test]
fn a_rolled_back_graft_carries_its_reason_and_no_diff() {
let mut s = state();
let mut g = graft_record(crate::run::GraftOutcome::RolledBack("too big".into()));
g.post = None;
s.graft = Some(g);
let v = graft_json(&s, None);
assert_eq!(v["graft"]["outcome"], "rolled_back");
assert_eq!(v["graft"]["reason"], "too big");
assert_eq!(v["graft"]["diff_available"], false);
assert_eq!(v["tone"], "warn");
}
#[test]
fn a_legacy_record_says_unavailable_and_claims_nothing() {
let mut s = state();
let mut g = graft_record(crate::run::GraftOutcome::Applied);
g.requested.clear();
g.adoption = None;
s.graft = Some(g);
let v = graft_json(&s, None);
let reasons: Vec<_> = v["graft"]["unavailable"]
.as_array()
.expect("a")
.iter()
.map(|u| u["reason"].as_str().expect("s"))
.collect();
assert_eq!(reasons, ["legacy_record", "legacy_record"]);
assert_eq!(v["graft"]["requested"].as_array().expect("a").len(), 0);
assert_eq!(v["graft"]["adopted_count"], 0);
}
#[test]
fn recovered_details_fill_a_legacy_record_and_are_marked() {
let mut s = state();
let mut g = graft_record(crate::run::GraftOutcome::Applied);
let requested = std::mem::take(&mut g.requested);
let adoption = g.adoption.take();
s.graft = Some(g);
let rec = crate::graft_report::Recovered {
requested,
adoption,
unavailable: Vec::new(),
};
let g = &graft_json(&s, Some(&rec))["graft"];
assert_eq!(g["recovered"], true);
assert_eq!(g["requested"][0]["state"], "adopted");
}
#[test]
fn agent_text_stays_text_in_the_view() {
let mut s = state();
let mut g = graft_record(crate::run::GraftOutcome::Applied);
g.requested[0].text = "<img src=x onerror=alert(1)>".into();
s.graft = Some(g);
let v = graft_json(&s, None);
assert_eq!(
v["graft"]["requested"][0]["text"],
"<img src=x onerror=alert(1)>"
);
let notes = v["graft"]["notes_md"].to_string();
assert!(!notes.contains("\"html\""), "{notes}");
}
}