1use std::fmt::Write as _;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use crate::config::{MergeMode, MergeStyle};
16use crate::run::{
17 CommandOutcome, ContinuationOutcome, E2eStatus, GateStatus, JobStatus, Liveness,
18 OperatorFixOutcome, RunState, RunStatus,
19};
20use crate::stats::{RepoStats, Stats};
21use crate::verdict::ReviewVote;
22
23static COLOR: AtomicBool = AtomicBool::new(true);
24
25pub fn set_color(on: bool) {
27 COLOR.store(on, Ordering::Relaxed);
28}
29
30fn paint(text: &str, code: &str) -> String {
31 if COLOR.load(Ordering::Relaxed) {
32 format!("\x1b[{code}m{text}\x1b[0m")
33 } else {
34 text.to_owned()
35 }
36}
37
38fn bold(t: &str) -> String {
39 paint(t, "1")
40}
41fn dim(t: &str) -> String {
42 paint(t, "2")
43}
44fn red(t: &str) -> String {
45 paint(t, "31")
46}
47fn green(t: &str) -> String {
48 paint(t, "32")
49}
50fn yellow(t: &str) -> String {
51 paint(t, "33")
52}
53fn cyan(t: &str) -> String {
54 paint(t, "36")
55}
56
57fn status_word(state: &RunState) -> String {
68 if state.unmerged_by_design() {
69 return cyan("unmerged (no-op by design)");
70 }
71 let text = state.status.display_label();
72 if state.needs_attention() {
73 return bold(&yellow(&format!("{text} - release needs a human")));
74 }
75 match state.status {
76 RunStatus::Merged => bold(&green(text)),
77 RunStatus::Ready => green(text),
78 RunStatus::Stalled => bold(&yellow(text)),
79 RunStatus::Blocked => yellow(text),
80 RunStatus::Failed => red(text),
81 RunStatus::VerifiedNoop => cyan(text),
85 RunStatus::Superseded => dim(text),
88 RunStatus::AlreadyInBase => dim(text),
91 _ => cyan(text),
92 }
93}
94
95fn vote_tag(vote: ReviewVote) -> String {
99 let text = vote.label();
100 match vote {
101 ReviewVote::Approve => green(text),
102 ReviewVote::ApproveWithFindings => yellow(text),
103 ReviewVote::Reject => red(text),
104 }
105}
106
107pub fn line(state: &RunState) -> String {
109 line_with_liveness(state, Liveness::Unknown)
110}
111
112pub fn line_with_liveness(state: &RunState, live: Liveness) -> String {
115 let winner = state
116 .tally
117 .as_ref()
118 .map_or("-".to_owned(), |t| t.winner.to_string());
119 let agent = state.winner().map_or("-", |c| c.agent.as_str());
120 let quorum = match state.tally.as_ref() {
123 Some(t) if !t.met_quorum => format!(
124 " {}",
125 bold(&red(&format!("quorum {}/{}", t.present, t.judges)))
126 ),
127 Some(t) if t.present > 0 && t.present < t.judges => format!(
128 " {}",
129 yellow(&format!("judges {}/{}", t.present, t.judges))
130 ),
131 _ => String::new(),
132 };
133 let stale = if let Some(startup) = state.startup.as_ref().filter(|s| !s.complete) {
134 format!(
135 " startup incomplete ({}) — inspect retained evidence; no ordinary resume",
136 startup.stage
137 )
138 } else if !state.status.done() && live == Liveness::Dead {
139 format!(
140 " {}",
141 bold(&yellow("STALE — driver exited; resume required"))
142 )
143 } else {
144 String::new()
145 };
146 let origin = state
149 .origin
150 .as_ref()
151 .map_or("origin unknown".to_owned(), crate::run::Origin::label);
152 format!(
153 "{} {:<20} {:>2}c {:>2}j win {} ({}){quorum}{stale} {} {}",
154 dim(&state.id),
155 status_word(state),
156 state.candidates.len(),
157 state.judgements.len(),
158 winner,
159 agent,
160 dim(&format!("[{origin}]")),
161 first_line(&state.instruction)
162 )
163}
164
165pub fn liveness_notice(state: &RunState, live: Liveness) -> String {
169 if state.startup.as_ref().is_some_and(|s| !s.complete) {
170 return "Startup incomplete; inspect startup evidence and retained assets. Ordinary resume is disabled.\n\n".to_owned();
171 }
172 if !state.status.done() && live == Liveness::Dead {
173 format!(
174 "{}\n\n",
175 yellow("STALE — the process driving this run exited; resume it to continue.")
176 )
177 } else {
178 String::new()
179 }
180}
181
182pub fn superseded_notice(later: Option<&str>) -> String {
193 match later {
194 Some(later) => format!(
195 "{}\n\n",
196 yellow(&format!(
197 "SUPERSEDED — a later attempt at this task ({later}) replaced it; this record \
198 is kept but is not the one to act on."
199 ))
200 ),
201 None => String::new(),
202 }
203}
204
205fn first_line(text: &str) -> String {
206 let line = text.lines().next().unwrap_or_default();
207 if line.chars().count() > 68 {
208 format!("{}…", line.chars().take(67).collect::<String>())
209 } else {
210 line.to_owned()
211 }
212}
213
214fn short(commit: &str) -> String {
215 commit.chars().take(7).collect()
216}
217
218fn continuation_note(c: &crate::run::ContinuationRecord) -> String {
223 match c.outcome {
224 ContinuationOutcome::NotNeeded => String::new(),
225 ContinuationOutcome::Resumed => format!(" [resumed x{}]", c.attempts),
226 ContinuationOutcome::Exhausted => format!(" [continuation exhausted x{}]", c.attempts),
227 ContinuationOutcome::QuotaLost => " [continuation: quota]".to_owned(),
228 ContinuationOutcome::NoSession => " [no session to resume]".to_owned(),
229 }
230}
231
232fn jobs_section(state: &RunState) -> String {
253 let mut s = String::new();
254 if state.jobs.is_empty() {
255 if state
260 .config
261 .agents
262 .iter()
263 .any(|a| a.kind == crate::config::AgentKind::Codex)
264 {
265 let _ = writeln!(
266 s,
267 "\n{}",
268 dim(
269 "background jobs: no completed command evidence yet for this run (see \
270 active seats above for what is still mid-turn)"
271 )
272 );
273 }
274 return s;
275 }
276 let _ = writeln!(
277 s,
278 "\n{}",
279 bold("background jobs (from each seat's own CLI)")
280 );
281 let mut by_seat: std::collections::BTreeMap<(&str, &str), Vec<&crate::run::JobRecord>> =
282 std::collections::BTreeMap::new();
283 for j in &state.jobs {
284 by_seat
285 .entry((j.node.as_str(), j.seat.as_str()))
286 .or_default()
287 .push(j);
288 }
289 for ((node, seat), records) in by_seat {
290 let _ = writeln!(s, " {node}/{seat}");
291 for j in records {
292 let status = match j.status {
293 JobStatus::Completed => green("completed"),
294 JobStatus::Failed => red("failed"),
295 JobStatus::Unknown => yellow("unknown"),
296 };
297 let _ = writeln!(
298 s,
299 " {} {}{}{} checked {}",
300 dim(&j.id),
301 status,
302 j.exit_code
303 .map_or(String::new(), |c| format!(" (exit {c})")),
304 j.round.map_or(String::new(), |r| format!(" round {r}")),
305 j.checked_at
306 .to_zoned(jiff::tz::TimeZone::system())
307 .strftime("%Y-%m-%d %H:%M:%S")
308 );
309 let desc = first_line(&j.description);
310 if !desc.trim().is_empty() {
311 let _ = writeln!(s, " $ {desc}");
312 }
313 let summary = first_line(&j.result_summary);
314 if !summary.trim().is_empty() {
315 let _ = writeln!(s, " {}", dim(&summary));
316 }
317 }
318 }
319 let _ = writeln!(
320 s,
321 " {}",
322 dim(
323 "(adapter coverage: codex only today; other backends, and a command a CLI never \
324 reported finishing, leave no entry here — that is unknown, never \"nothing ran\")"
325 )
326 );
327 s
328}
329
330pub fn run(state: &RunState) -> String {
332 let mut s = String::new();
333 let _ = writeln!(
334 s,
335 "{} {} {}",
336 bold("magi run"),
337 bold(&state.id),
338 status_word(state)
339 );
340 let _ = writeln!(
341 s,
342 " repo {} ({} @ {})",
343 state.repo.display(),
344 state.base_branch,
345 short(&state.base_commit)
346 );
347 let _ = writeln!(s, " created {}", state.created_local());
348 let _ = writeln!(s, " task {}", first_line(&state.instruction));
349 let _ = writeln!(
350 s,
351 " origin {}",
352 crate::run::origin_label(state.origin.as_ref())
353 );
354 if let Some(startup) = &state.startup {
355 let _ = writeln!(
356 s,
357 " startup {} branch={} stage={} complete={}",
358 startup.kind,
359 startup.branch.as_deref().unwrap_or("-"),
360 startup.stage,
361 startup.complete
362 );
363 if let Some(failure) = &startup.failure {
364 let _ = writeln!(s, " startup failure {failure}");
365 }
366 }
367 let _ = writeln!(s, " state {}", state.dir().display());
368
369 let _ = writeln!(s, "\n{}", bold("candidates"));
370 for c in &state.candidates {
371 let flag = match (&c.failed, c.empty, &c.verified_noop) {
372 (Some(e), _, _) => red(&format!("failed: {e}")),
373 (None, true, Some(_)) => cyan("agent-verified no-op (unconfirmed)"),
377 (None, true, None) => yellow("no change"),
378 _ => format!("{} files, {} commits", c.files, c.commits),
379 };
380 let crown = if state.tally.as_ref().is_some_and(|t| t.winner == c.label) {
381 bold(&green(" <- winner"))
382 } else {
383 String::new()
384 };
385 let _ = writeln!(
386 s,
387 " {} {:<12} {:<30} {:>5}s{}",
388 bold(&c.label.to_string()),
389 c.agent,
390 flag,
391 c.duration_ms / 1000,
392 crown
393 );
394 if let Some(evidence) = &c.verified_noop {
395 let _ = writeln!(s, " {}", dim(&first_line(evidence)));
396 } else if !c.summary.trim().is_empty() {
397 let _ = writeln!(s, " {}", dim(&first_line(&c.summary)));
406 }
407 }
408
409 if !state.judgements.is_empty() {
410 let _ = writeln!(s, "\n{}", bold("blind judging"));
411 for j in &state.judgements {
412 match &j.failed {
413 Some(e) => {
414 let _ = writeln!(
415 s,
416 " judge {} {}",
417 j.judge,
418 red(&format!("no ranking: {e}"))
419 );
420 }
421 None => {
422 let _ = writeln!(
423 s,
424 " judge {} {:<12} {} confidence {}",
425 j.judge,
426 j.agent,
427 bold(&j.ranking.iter().collect::<String>()),
428 j.confidence.map_or("-".to_owned(), |c| c.to_string())
429 );
430 }
431 }
432 }
433 }
434
435 if let Some(t) = &state.tally {
436 if t.deliberated {
437 let _ = writeln!(s, "\n{}", bold("deliberation"));
438 for round in &state.deliberation {
439 for turn in &round.turns {
440 let _ = writeln!(
441 s,
442 " r{} judge {} -> {}",
443 round.round,
444 turn.judge,
445 turn.tentative.map_or("-".to_owned(), |c| c.to_string())
446 );
447 }
448 }
449 }
450
451 if !state.votes.is_empty() {
452 let _ = writeln!(s, "\n{}", bold("final votes (collected privately)"));
453 for v in &state.votes {
454 let _ = writeln!(
455 s,
456 " judge {} {:<12} {}{}",
457 v.judge,
458 v.agent,
459 bold(&v.vote.unwrap_or('?').to_string()),
460 if v.changed {
461 yellow(" (changed after deliberation)")
462 } else {
463 String::new()
464 }
465 );
466 }
467 }
468
469 let _ = writeln!(s, "\n{}", bold("tally"));
470 match &t.uncontested {
476 Some(reason) => {
477 let _ = writeln!(
478 s,
479 " judging {}",
480 cyan(&format!("not needed — {reason}"))
481 );
482 }
483 None => {
484 let _ = writeln!(
485 s,
486 " judges {} present{}",
487 if t.met_quorum {
488 green(&format!("{}/{}", t.present, t.judges))
489 } else {
490 red(&format!("{}/{}", t.present, t.judges))
491 },
492 if t.quorum > 0 {
493 format!(" ({quorum} required)", quorum = t.quorum)
494 } else {
495 String::new()
496 }
497 );
498 if !t.met_quorum {
499 let _ = writeln!(
500 s,
501 " {}",
502 bold(&red("BELOW QUORUM — verdict is not trustworthy"))
503 );
504 }
505 let _ = writeln!(
506 s,
507 " first choice {}",
508 t.first_choice
509 .iter()
510 .map(|(k, v)| format!("{k}:{v}"))
511 .collect::<Vec<_>>()
512 .join(" ")
513 );
514 let _ = writeln!(
515 s,
516 " initial {}",
517 match (t.rankings, t.unanimous_initial) {
518 (0, _) => red("no usable ranking"),
519 (1, _) => yellow("one usable ranking - not a consensus"),
520 (_, true) => green("unanimous"),
521 (_, false) => yellow("split"),
522 }
523 );
524 let _ = writeln!(
525 s,
526 " after votes {} ({} judge(s) moved)",
527 if t.unanimous_final {
528 green("unanimous")
529 } else {
530 yellow("still split")
531 },
532 t.changed_votes
533 );
534 if let Some(tb) = &t.tie_break {
535 let _ = writeln!(s, " tie break {tb}");
536 }
537 }
538 }
539 if !state.quota.is_empty() {
540 let _ = writeln!(
541 s,
542 " unavailable {}",
543 state
544 .quota
545 .iter()
546 .map(|q| if q.cause == crate::agent::QuotaCause::Authentication {
547 format!("{} ({}: re-authenticate)", q.seat, q.agent)
548 } else {
549 format!("{} (usage limit)", q.seat)
550 })
551 .collect::<Vec<_>>()
552 .join(", ")
553 );
554 }
555 let _ = writeln!(s, " winner {}", bold(&green(&t.winner.to_string())));
556 }
557
558 if !state.handovers.is_empty() {
559 let _ = writeln!(s, "\n{}", bold("seats handed over"));
560 for h in &state.handovers {
561 let _ = writeln!(
562 s,
563 " {} {} {} -> {} ({}: {})",
564 yellow("~"),
565 h.seat,
566 h.from,
567 h.to,
568 h.node,
569 h.reason
570 );
571 }
572 }
573
574 if !state.withheld.is_empty() {
575 let _ = writeln!(s, "\n{}", bold("withheld from commit"));
576 for w in &state.withheld {
577 let _ = writeln!(
578 s,
579 " {} {} ({} lockfile; the directory uses {})",
580 yellow("!"),
581 w.path,
582 w.manager,
583 w.kept_by
584 );
585 }
586 }
587
588 if let Some(r) = &state.recovery {
589 let _ = writeln!(s, "\n{}", bold("recovery"));
590 let _ = writeln!(s, " {}", r.summary());
591 if r.criteria_inherited {
592 let _ = writeln!(s, " the task's own completion criteria were kept");
593 }
594 match &r.repair {
595 crate::recovery::Repair::NotStarted => {}
596 crate::recovery::Repair::WaitingForRestore { .. } => {
597 let _ = writeln!(s, " repair: waiting to restore and retry");
598 }
599 crate::recovery::Repair::Started { .. } => {
600 let _ = writeln!(s, " repair: {}", yellow("unfinished"));
601 }
602 crate::recovery::Repair::Done {
603 committed, failed, ..
604 } => {
605 let _ = match failed {
606 Some(why) => writeln!(s, " repair: {} {why}", yellow("nothing kept:")),
607 None if *committed => writeln!(s, " repair: {}", green("committed")),
608 None => writeln!(s, " repair: no commit"),
609 };
610 }
611 }
612 }
613
614 if let Some(g) = &state.graft {
615 let _ = writeln!(s, "\n{}", bold("graft"));
616 let size = g.lines.map_or_else(
617 || " changed lines unavailable".to_owned(),
618 |n| format!(" {n} changed lines"),
619 );
620 let _ = match &g.outcome {
621 crate::run::GraftOutcome::Applied => writeln!(s, " {}{size}", green("applied")),
622 crate::run::GraftOutcome::WaitingForRestore => {
623 writeln!(s, " {}{size}", yellow("waiting to restore and retry"))
624 }
625 crate::run::GraftOutcome::Running => writeln!(s, " {}{size}", yellow("unfinished")),
626 crate::run::GraftOutcome::RolledBack(why) => {
627 writeln!(s, " {} {why}{size}", yellow("rolled back:"))
628 }
629 };
630 if let Some(a) = &g.agent {
631 let chain: Vec<String> = state
632 .handovers
633 .iter()
634 .filter(|h| h.node == "graft")
635 .map(|h| format!("{} -> {}", h.from, h.to))
636 .collect();
637 let via = if chain.is_empty() {
638 String::new()
639 } else {
640 format!(" (handovers: {})", chain.join(", "))
641 };
642 let _ = writeln!(s, " by {a}{via}");
643 }
644 if !g.requested.is_empty() {
645 let view = crate::report_view::build(state, Liveness::Unknown);
646 if let Some(crate::report_view::Section::Graft { graft, .. }) = view
647 .sections
648 .iter()
649 .find(|x| matches!(x, crate::report_view::Section::Graft { .. }))
650 {
651 let _ = writeln!(
652 s,
653 " requested {} confirmed adopted {} rejected {} unconfirmed {}",
654 graft.requested.len(),
655 graft.adopted_count,
656 graft.rejected_count,
657 graft.unconfirmed_count
658 );
659 }
660 }
661 if let Some(n) = g
662 .adoption
663 .as_ref()
664 .and_then(|a| a.notes.lines().find(|l| !l.trim().is_empty()))
665 {
666 let _ = writeln!(s, " fixer notes: {}", n.trim());
667 }
668 }
669
670 if !state.reviews.is_empty() {
671 let _ = writeln!(s, "\n{}", bold("review + verification"));
672 for r in &state.reviews {
673 let raised: usize = r.reviews.iter().map(|x| x.findings.len()).sum();
674 let e2e = match r.e2e_status() {
682 E2eStatus::NotConfigured => dim("no e2e"),
683 E2eStatus::Deferred => yellow(&format!(
684 "e2e deferred{}",
685 r.e2e_defer_reason
686 .as_deref()
687 .map(|why| format!(" ({why})"))
688 .unwrap_or_default()
689 )),
690 E2eStatus::Passed => green("e2e green"),
691 E2eStatus::Failed if r.e2e.iter().any(CommandOutcome::build_failed) => {
692 yellow("e2e could not run (build/link failure)")
693 }
694 E2eStatus::Failed => red("e2e RED"),
695 E2eStatus::ResourceBlocked => yellow("e2e could not run (resource unavailable)"),
700 };
701 let e2e = if r.verify_retried {
702 format!("{e2e}, retried once")
703 } else {
704 e2e
705 };
706 let status = if r.incomplete() {
710 yellow("incomplete")
711 } else if r.clean {
712 green("clean")
713 } else {
714 yellow("open")
715 };
716 let panel = if r.incomplete() {
720 let missing: Vec<String> = r
721 .reviews
722 .iter()
723 .filter_map(|x| {
724 x.failed
725 .as_ref()
726 .map(|why| format!("review-{}: {why}", x.reviewer))
727 })
728 .collect();
729 format!(
730 " {}/{} reviewers answered ({})",
731 r.answered,
732 r.expected,
733 missing.join(", ")
734 )
735 } else {
736 String::new()
737 };
738 let verdict = r.verdict.map_or(String::new(), |v| {
744 format!(
745 ", verdict {}{}",
746 vote_tag(v),
747 if r.vote_split { " (panel split)" } else { "" }
748 )
749 });
750 let _ = writeln!(
751 s,
752 " round {} {} @ {}{}{panel} {raised} finding(s), {} blocking, {e2e}{verdict}{}",
753 r.round,
754 status,
755 short(&r.head),
756 r.verified_head.as_ref().map_or(String::new(), |head| {
757 format!(
758 " (verified @ {}{})",
759 short(head),
760 r.verified_at.map_or(String::new(), |t| format!(
761 " on {}",
762 t.to_zoned(jiff::tz::TimeZone::system())
763 .strftime("%Y-%m-%d %H:%M:%S")
764 ))
765 )
766 }),
767 r.blocking,
768 r.fix.as_ref().map_or(String::new(), |f| {
769 let tree = if r.progressed {
770 green("changed")
771 } else {
772 yellow("unchanged")
773 };
774 let cont = f
775 .continuation
776 .as_ref()
777 .map_or(String::new(), continuation_note);
778 match &f.failed {
779 Some(reason) => format!(
784 " fix: {}, tree {tree}{}{cont}",
785 yellow(&format!("adoption report lost ({reason})")),
786 if f.committed {
787 String::new()
788 } else {
789 red(" (NO COMMIT)")
790 }
791 ),
792 None => format!(
793 " fix: {} addressed / {} rejected, tree {tree}{}{cont}",
794 f.addressed.len(),
795 f.rejected.len(),
796 if f.committed {
797 String::new()
798 } else {
799 red(" (NO COMMIT)")
800 }
801 ),
802 }
803 })
804 );
805 for o in &r.e2e {
812 let label = if o.resource_blocked {
813 yellow("blocked")
814 } else if o.ok() {
815 green("pass")
816 } else {
817 red("FAIL")
818 };
819 let _ = writeln!(s, " {label} {}", o.command);
820 if !o.ok() {
821 let _ = writeln!(
822 s,
823 "{}",
824 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
825 );
826 }
827 }
828 for rec in &r.reviews {
829 if let Some(vote) = rec.vote {
830 let _ = writeln!(s, " review-{} vote {}", rec.reviewer, vote_tag(vote));
831 }
832 for f in &rec.findings {
833 let adopted = r
834 .fix
835 .as_ref()
836 .is_some_and(|fix| fix.addressed.contains(&f.id));
837 let _ = writeln!(
838 s,
839 " {} [{:?}] {}{}",
840 dim(&f.id),
841 f.severity,
842 f.title,
843 if adopted {
844 green(" fixed")
845 } else {
846 String::new()
847 }
848 );
849 }
850 }
851 if let Some(fix) = &r.fix {
852 for rej in &fix.rejected {
853 let _ = writeln!(
854 s,
855 " {} {}: {}",
856 dim(&rej.id),
857 yellow("declined"),
858 rej.why
859 );
860 }
861 }
862 if !r.reconsideration.is_empty() {
866 let _ = writeln!(s, " {}", dim("reconsideration:"));
867 for rv in &r.reconsideration {
868 match rv.vote {
869 Some(v) => {
870 let _ = writeln!(
871 s,
872 " review-{} -> {} {}",
873 rv.reviewer,
874 vote_tag(v),
875 rv.reason
876 );
877 }
878 None => {
879 let _ = writeln!(
880 s,
881 " review-{} -> {}",
882 rv.reviewer,
883 red(&format!(
884 "no revote ({})",
885 rv.failed.as_deref().unwrap_or("unknown")
886 ))
887 );
888 }
889 }
890 }
891 }
892 }
893 if state.handed_off_with_open_findings() {
894 let _ = writeln!(
895 s,
896 "\n {}",
897 yellow(&format!(
898 "handed off with {} finding(s) still open — gate and e2e were green; \
899 see above for what a person should still look at",
900 state.open_findings().len()
901 ))
902 );
903 }
904 }
905
906 if !state.merge_repairs.is_empty() {
907 let _ = writeln!(s, "\n{}", bold("merge-gate repairs"));
908 for r in &state.merge_repairs {
909 let _ = writeln!(
910 s,
911 " question {} head {} {:?}\n {}",
912 r.question,
913 crate::run::short_of(&r.head),
914 r.phase,
915 r.pr
916 );
917 if let Some(why) = &r.failure {
918 let _ = writeln!(s, " held: {why}");
919 }
920 if let Some(round) = r.reviews.last() {
921 for f in round.reviews.iter().flat_map(|rec| &rec.findings) {
922 let _ = writeln!(s, " original [{}] {:?}: {}", f.id, f.severity, f.title);
923 }
924 for (seat, _, vote) in round.final_votes() {
925 let _ = writeln!(s, " original reviewer #{seat}: {:?}", vote);
926 }
927 }
928 }
929 s.push_str(
930 " Repair never approves a merge; updated results require a fresh owner decision.\n",
931 );
932 }
933
934 if !state.operator_fixes.is_empty() {
935 let _ = writeln!(s, "\n{}", bold("operator fix(es)"));
936 for (i, req) in state.operator_fixes.iter().enumerate() {
937 let _ = writeln!(
938 s,
939 " [{}] {} finding(s) at {}{}",
940 i + 1,
941 req.findings.len(),
942 req.requested_at
943 .to_zoned(jiff::tz::TimeZone::system())
944 .strftime("%Y-%m-%d %H:%M:%S"),
945 if req.stale {
946 yellow(" stale head, --allow-stale used")
947 } else {
948 String::new()
949 }
950 );
951 let _ = writeln!(s, " reason: {}", req.reason);
952 for f in &req.findings {
953 let outcome = match &f.outcome {
954 OperatorFixOutcome::Pending => yellow("pending"),
955 OperatorFixOutcome::Addressed => green("addressed"),
956 OperatorFixOutcome::Rejected { why } => red(&format!("rejected: {why}")),
957 OperatorFixOutcome::Unreported => {
958 red("unreported — no adoption report came back")
959 }
960 };
961 let _ = writeln!(
962 s,
963 " {} [{:?}] {} {outcome}",
964 dim(&f.id),
965 f.severity,
966 f.title
967 );
968 }
969 match &req.follow_up_review_run {
970 Some(id) => {
971 let _ = writeln!(s, " re-verified by run {id}");
972 }
973 None if req.fix.as_ref().is_some_and(|fx| fx.committed) => {
974 let _ = writeln!(
975 s,
976 " {}",
977 red("committed, but the follow-up review could not be opened")
978 );
979 }
980 None => {
981 let _ = writeln!(s, " no change committed; nothing to re-verify");
982 }
983 }
984 }
985 }
986
987 if let Some(bs) = &state.base_sync {
988 let _ = writeln!(s, "\n{}", bold("base sync"));
989 let status = if let Some(a) = &bs.already_in {
990 dim(&format!(
991 "already in {} as {} ({} match) - nothing left to land",
992 state.base_branch,
993 a.names(),
994 a.proof.as_str()
995 ))
996 } else if let Some(c) = &bs.conflict {
997 red(&format!("stopped: {}", first_line(c)))
998 } else if bs.behind == 0 {
999 green("in sync")
1000 } else {
1001 yellow(&format!("{} commit(s) behind, not yet rebased", bs.behind))
1002 };
1003 let _ = writeln!(
1004 s,
1005 " {} @ {} {status}{}",
1006 state.base_branch,
1007 short(&bs.tip),
1008 if bs.attempts > 0 {
1009 format!(
1010 " (interval {}/{}, lifetime {} synchronization attempt(s))",
1011 state.base_episode_used(),
1012 crate::graph::BASE_SYNC_ROUNDS,
1013 bs.attempts
1014 )
1015 } else {
1016 String::new()
1017 }
1018 );
1019 }
1020
1021 if state.base_sync.is_some() {
1022 let _ = writeln!(s, " next: {}", state.base_sync_next_action());
1023 }
1024
1025 if !state.pre_gate.is_empty() {
1026 let _ = writeln!(s, "\n{}", bold("pre_gate"));
1027 for o in &state.pre_gate {
1028 let _ = writeln!(
1029 s,
1030 " {} {}",
1031 if o.ok() {
1032 green("pass")
1033 } else {
1034 yellow("warn")
1035 },
1036 o.command
1037 );
1038 if !o.ok() {
1039 let _ = writeln!(
1040 s,
1041 "{}",
1042 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
1043 );
1044 }
1045 }
1046 if let Some(c) = &state.pre_gate_commit {
1047 let _ = writeln!(s, " committed mechanical fixes @ {}", short(c));
1048 }
1049 }
1050
1051 match state.gate_status() {
1055 GateStatus::NotRun => {}
1056 GateStatus::NotConfigured => {
1057 let _ = writeln!(s, "\n{}", bold("gate"));
1058 let _ = writeln!(s, " {} no gate commands configured", dim("not tested"));
1059 }
1060 GateStatus::Passed | GateStatus::Failed => {
1061 let _ = writeln!(s, "\n{}", bold("gate"));
1062 for o in &state.gate {
1063 let _ = writeln!(
1064 s,
1065 " {} {}",
1066 if o.ok() { green("pass") } else { red("FAIL") },
1067 o.command
1068 );
1069 if !o.ok() {
1070 let _ = writeln!(
1071 s,
1072 "{}",
1073 dim(&crate::run::summarize_verify(&o.output_tail, 2_000))
1074 );
1075 }
1076 }
1077 }
1078 }
1079
1080 if let Some(m) = &state.merge {
1081 let _ = writeln!(s, "\n{}", bold("merge"));
1082 if m.mode == MergeMode::None {
1083 let _ = writeln!(
1087 s,
1088 " mode None {}",
1089 cyan("not landed — nothing to do by design")
1090 );
1091 if let Some(w) = state.winner() {
1092 let _ = writeln!(
1093 s,
1094 " branch {} still exists, unmerged into {}",
1095 w.branch, state.base_branch
1096 );
1097 }
1098 let _ = writeln!(
1104 s,
1105 " rebase onto {} before merging by hand{}",
1106 state.base_branch,
1107 if state.config.merge.style == MergeStyle::Squash {
1108 ", and pass an explicit commit message — a squash merge \
1109 otherwise inherits the candidate's placeholder subject"
1110 } else {
1111 ""
1112 }
1113 );
1114 let _ = writeln!(s, " {}", m.detail.lines().next().unwrap_or(""));
1115 } else {
1116 let _ = writeln!(
1117 s,
1118 " mode {:?} {}\n {}",
1119 m.mode,
1120 if m.empty {
1121 yellow("empty candidate — nothing to land")
1122 } else if m.ok && state.status == RunStatus::Merged {
1123 green("merged")
1124 } else if m.ok && m.mode == MergeMode::Pr {
1125 yellow("published; not merged")
1126 } else {
1127 yellow("not merged")
1128 },
1129 m.detail.lines().next().unwrap_or("")
1130 );
1131 }
1132 if let Some(pr) = &state.pr
1133 && !pr.red_at_merge.is_empty()
1134 {
1135 let _ = writeln!(
1136 s,
1137 " {} {}",
1138 yellow("merged with red checks:"),
1139 pr.red_at_merge.join(", ")
1140 );
1141 }
1142 }
1143
1144 if !state.followups.is_empty() {
1145 let _ = writeln!(s, "\n{}", bold("follow-ups"));
1146 for f in &state.followups {
1147 let _ = writeln!(
1148 s,
1149 " {} {}",
1150 crate::queue::short(&f.task),
1151 f.findings.join(", ")
1152 );
1153 }
1154 }
1155
1156 if !state.bump_decision_attempts.is_empty() {
1157 let _ = writeln!(s, "\n{}", bold("release decision attempts"));
1158 for (n, attempt) in state.bump_decision_attempts.iter().enumerate() {
1159 let _ = writeln!(s, " {}. {}: {}", n + 1, attempt.agent, attempt.outcome);
1160 }
1161 }
1162 if let Some(b) = &state.release_bump {
1163 let _ = writeln!(s, "\n{}", bold("release bump"));
1164 if let Some(why) = &b.skipped {
1165 let _ = writeln!(
1166 s,
1167 " {} {why}",
1168 bold(&yellow("SKIPPED (no release was made):"))
1169 );
1170 }
1171 if let Some(v) = &b.version {
1172 let _ = writeln!(s, " version v{v}");
1173 }
1174 if let Some(url) = &b.pr_url {
1175 let _ = writeln!(s, " pr {url}");
1176 }
1177 if b.skipped.is_none() || b.pr_url.is_some() {
1178 let _ = writeln!(
1179 s,
1180 " automerge {}",
1181 if b.automerge_enabled {
1182 green("enabled")
1183 } else if b.merged_directly {
1184 green("not needed (CI was already green; merged directly)")
1185 } else if b.local {
1186 dim(
1187 "not used (release.mode = local: merged on your approval, released by magi serve)",
1188 )
1189 } else if b.problem.is_some() {
1190 bold(&red("FAILED"))
1191 } else {
1192 dim("not enabled")
1193 }
1194 );
1195 }
1196 if let Some(p) = &b.problem {
1197 let _ = writeln!(s, " {}", p.lines().next().unwrap_or(""));
1198 }
1199 if let Some(j) = &b.release {
1200 let state = if j.finished {
1201 "finished"
1202 } else {
1203 j.failed.as_deref().unwrap_or("unfinished")
1204 };
1205 let _ = writeln!(s, " release {} at {}: {state}", j.tag(), j.commit);
1206 for l in &j.log {
1207 let code = l.code.map_or("none".to_owned(), |c| c.to_string());
1208 let _ = write!(s, " {} (exit {code})", l.name);
1209 if let Some(o) = &l.output {
1210 let _ = write!(s, " output {o}");
1211 }
1212 let _ = writeln!(s);
1213 }
1214 }
1215 if let Some(a) = &b.action_required {
1216 let _ = writeln!(s, " {} {a}", bold(&yellow("action required:")));
1217 }
1218 }
1219
1220 if !state.leaks.is_empty() {
1221 let _ = writeln!(s, "\n{}", bold(&yellow("blindness warnings")));
1222 for l in &state.leaks {
1223 let _ = writeln!(s, " {} x{} in {}", l.token, l.count, l.site);
1224 }
1225 }
1226
1227 if let Some(w) = state.winner()
1228 && !w.folded
1229 {
1230 let _ = writeln!(
1231 s,
1232 "\n{} {}\n branch {}",
1233 bold("winner worktree"),
1234 w.worktree.display(),
1235 w.branch
1236 );
1237 }
1238 s.push_str(&jobs_section(state));
1239 s
1240}
1241
1242pub fn active_seats(state: &RunState, live: Liveness) -> String {
1258 let mut s = String::new();
1259 if let Some(block) = &state.resource_block {
1260 let _ = writeln!(
1261 s,
1262 "\n{}\n {}. Resume to retry the affected operation.",
1263 bold("waiting on a resource"),
1264 block.description()
1265 );
1266 }
1267 if state.active.is_empty() {
1268 return s;
1269 }
1270 let _ = writeln!(s, "\n{}", bold("running now"));
1271 let now = jiff::Timestamp::now();
1272 match live {
1273 Liveness::Live => {}
1274 Liveness::Dead => {
1275 let _ = writeln!(
1276 s,
1277 " {}",
1278 yellow(
1279 "no live daemon claims this run right now — likely left behind by a killed process"
1280 )
1281 );
1282 }
1283 Liveness::Unknown => {
1284 let overrun = if state.active_all_overrun(now) {
1290 " — every active seat has already run past its own timeout budget"
1291 } else {
1292 ""
1293 };
1294 let _ = writeln!(
1295 s,
1296 " {}",
1297 yellow(&format!(
1298 "whether a process is still driving this run could not be confirmed{overrun}"
1299 ))
1300 );
1301 }
1302 }
1303 for (seat, a) in state.seats_active() {
1304 let retry = if a.attempt > 0 {
1305 format!(" retry {}", a.attempt)
1306 } else {
1307 String::new()
1308 };
1309 if let Some(w) = state.resource_wait.as_ref().filter(|w| w.node == a.node) {
1311 let _ = writeln!(
1312 s,
1313 " {:<12} {:<12}{retry} waiting for the {} ({}), {}s of {}s; agent not started yet",
1314 seat,
1315 a.node,
1316 w.kind,
1317 w.holder,
1318 (now.as_second() - w.since.as_second()).max(0),
1319 w.limit_secs
1320 );
1321 continue;
1322 }
1323 let _ = writeln!(
1324 s,
1325 " {:<12} {:<12}{retry} {}s elapsed, {}s left of {}s",
1326 seat,
1327 a.node,
1328 a.elapsed_secs(now),
1329 a.remaining_secs(now),
1330 a.timeout_secs
1331 );
1332 }
1333 for (task, a) in state.tasks_active() {
1334 let retry = if a.attempt > 0 {
1335 format!(" retry {}", a.attempt)
1336 } else {
1337 String::new()
1338 };
1339 let progress = match (a.index, a.total) {
1340 (Some(i), Some(t)) => format!(" ({i}/{t})"),
1341 _ => String::new(),
1342 };
1343 let _ = writeln!(
1344 s,
1345 " {:<12} {:<12}{retry}{progress} {}s elapsed, {}s left of {}s",
1346 task,
1347 a.node,
1348 a.elapsed_secs(now),
1349 a.remaining_secs(now),
1350 a.timeout_secs
1351 );
1352 if let Some(command) = &a.command {
1353 let _ = writeln!(s, " {command}");
1354 }
1355 }
1356 s
1357}
1358
1359fn fmt_secs(secs: i64) -> String {
1361 let secs = secs.max(0);
1362 let h = secs / 3600;
1363 let m = (secs % 3600) / 60;
1364 let s = secs % 60;
1365 if h > 0 {
1366 format!("{h}h{m:02}m")
1367 } else if m > 0 {
1368 format!("{m}m{s:02}s")
1369 } else {
1370 format!("{s}s")
1371 }
1372}
1373
1374fn bar(value: f64, max: f64, width: usize) -> String {
1379 if max <= 0.0 || value <= 0.0 {
1380 return String::new();
1381 }
1382 let cells = ((value / max) * width as f64)
1383 .round()
1384 .clamp(1.0, width as f64) as usize;
1385 "█".repeat(cells)
1386}
1387
1388pub fn repo_summary(repos: &[RepoStats]) -> String {
1394 let mut s = String::new();
1395 let _ = writeln!(s, "{}", bold("repos"));
1396
1397 let mut name_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
1401 for r in repos {
1402 *name_counts.entry(r.name.as_str()).or_insert(0) += 1;
1403 }
1404
1405 for r in repos {
1406 let label = if name_counts.get(r.name.as_str()).copied().unwrap_or(0) > 1 {
1407 match r.repo.parent().and_then(|p| p.file_name()) {
1408 Some(parent) => format!("{} ({})", r.name, parent.to_string_lossy()),
1409 None => r.name.clone(),
1410 }
1411 } else {
1412 r.name.clone()
1413 };
1414 let _ = writeln!(
1415 s,
1416 " {} {} runs {:.0}% completion",
1417 label,
1418 r.stats.totals.runs,
1419 r.stats.totals.completion_rate()
1420 );
1421 }
1422 s
1423}
1424
1425pub fn stats(stats: &Stats) -> String {
1427 let t = &stats.totals;
1428 let mut s = String::new();
1429 let _ = writeln!(s, "{}", bold("runs"));
1430 let _ = writeln!(
1431 s,
1432 " {} total - {} merged, {} ready, {} blocked, {} failed ({:.0}% completion)",
1433 t.runs,
1434 t.merged,
1435 t.ready,
1436 t.blocked,
1437 t.failed,
1438 t.completion_rate()
1439 );
1440 if t.tallied > 0 {
1441 let _ = writeln!(
1442 s,
1443 " {} tallied - {} split on first choice ({:.0}%), {} deliberated, \
1444 {} of those changed a mind, {} converged to unanimous",
1445 t.tallied,
1446 t.split,
1447 t.split_rate(),
1448 t.deliberated,
1449 t.minds_changed,
1450 t.converged
1451 );
1452 }
1453
1454 if !stats.retired_hidden.is_empty() {
1455 let n = stats.retired_hidden.len();
1456 let _ = writeln!(
1457 s,
1458 "{n} retired agent{} hidden: {} (magi stats --all to show)\n",
1459 if n == 1 { "" } else { "s" },
1460 stats.retired_hidden.join(", ")
1461 );
1462 }
1463
1464 let has_competitive = stats.agents.iter().any(|a| a.competitive_entered > 0);
1465 if has_competitive {
1466 let mut comp_agents = stats.agents.clone();
1467 comp_agents.retain(|a| a.competitive_entered > 0);
1468 comp_agents.sort_by(|a, b| {
1469 let a_tier = if a.competitive_entered < 10 { 1 } else { 0 };
1470 let b_tier = if b.competitive_entered < 10 { 1 } else { 0 };
1471 a_tier
1472 .cmp(&b_tier)
1473 .then(
1474 b.competitive_win_rate()
1475 .total_cmp(&a.competitive_win_rate()),
1476 )
1477 .then(b.competitive_entered.cmp(&a.competitive_entered))
1478 .then(a.agent.cmp(&b.agent))
1479 });
1480
1481 let _ = writeln!(
1482 s,
1483 "\n{}",
1484 bold("implementation (competitive runs, 2+ candidates)")
1485 );
1486 let _ = writeln!(
1487 s,
1488 " {:<14}{:>6}{:>8}{:>8}{:>8}",
1489 "agent", "won", "entered", "rate", "empty"
1490 );
1491 for a in &comp_agents {
1492 let low_n_marker = if a.competitive_entered < 10 {
1493 format!(" {}", dim("low n"))
1494 } else {
1495 String::new()
1496 };
1497 let _ = writeln!(
1498 s,
1499 " {:<14}{:>6}{:>8}{:>7.0}%{:>8}{}",
1500 a.agent,
1501 a.competitive_wins,
1502 a.competitive_entered,
1503 a.competitive_win_rate(),
1504 a.empty,
1505 low_n_marker
1506 );
1507 }
1508 }
1509
1510 let has_solo = stats.agents.iter().any(|a| a.solo_entered > 0);
1511 if has_solo {
1512 let mut solo_agents = stats.agents.clone();
1513 solo_agents.retain(|a| a.solo_entered > 0);
1514 solo_agents.sort_by(|a, b| {
1515 let a_tier = if a.solo_entered < 10 { 1 } else { 0 };
1516 let b_tier = if b.solo_entered < 10 { 1 } else { 0 };
1517 a_tier
1518 .cmp(&b_tier)
1519 .then(b.solo_adoption_rate().total_cmp(&a.solo_adoption_rate()))
1520 .then(b.solo_entered.cmp(&a.solo_entered))
1521 .then(a.agent.cmp(&b.agent))
1522 });
1523
1524 let _ = writeln!(
1525 s,
1526 "\n{}",
1527 bold("implementation (solo runs, completion/adoption rate)")
1528 );
1529 let _ = writeln!(
1530 s,
1531 " {:<14}{:>8}{:>8}{:>8}",
1532 "agent", "adopted", "entered", "rate"
1533 );
1534 for a in &solo_agents {
1535 let low_n_marker = if a.solo_entered < 10 {
1536 format!(" {}", dim("low n"))
1537 } else {
1538 String::new()
1539 };
1540 let _ = writeln!(
1541 s,
1542 " {:<14}{:>8}{:>8}{:>7.0}%{}",
1543 a.agent,
1544 a.solo_adopted,
1545 a.solo_entered,
1546 a.solo_adoption_rate(),
1547 low_n_marker
1548 );
1549 }
1550 }
1551
1552 if !stats.reviewers.is_empty() {
1553 let _ = writeln!(s, "\n{}", bold("review"));
1554 let _ = writeln!(
1555 s,
1556 " {:<14}{:>8}{:>10}{:>11}{:>9}{:>9}{:>9}",
1557 "reviewer", "rounds", "submitted", "adopted/rd", "precision", "unique", "timeout"
1558 );
1559 for r in &stats.reviewers {
1560 let _ = writeln!(
1561 s,
1562 " {:<14}{:>8}{:>10}{:>11.2}{:>8.0}%{:>8.0}%{:>8.0}%",
1563 r.agent,
1564 r.rounds,
1565 r.submitted,
1566 r.adopted_per_round(),
1567 r.precision(),
1568 r.unique_rate(),
1569 r.timeout_rate()
1570 );
1571 }
1572 }
1573
1574 if !stats.advisors.is_empty() {
1575 let _ = writeln!(
1576 s,
1577 "\n{} {}",
1578 bold("design deliberation"),
1579 dim(
1580 "(approximate: reflection is read off word overlap with the synthesis brief, not a precise attribution)"
1581 )
1582 );
1583 let _ = writeln!(
1584 s,
1585 " {:<14}{:>7}{:>10}{:>8}{:>8}{:>9}{:>10}",
1586 "advisor", "seats", "proposed", "strong", "faint", "absent", "reflect%"
1587 );
1588 for a in &stats.advisors {
1589 let _ = writeln!(
1590 s,
1591 " {:<14}{:>7}{:>10}{:>8}{:>8}{:>9}{:>9.0}%",
1592 a.agent,
1593 a.seated,
1594 a.proposed,
1595 a.strong,
1596 a.faint,
1597 a.absent,
1598 a.reflection_rate()
1599 );
1600 }
1601 }
1602
1603 if stats.release_bumps.merged > 0 {
1604 let b = &stats.release_bumps;
1605 let _ = writeln!(s, "\n{}", bold("release bumps"));
1606 let _ = writeln!(
1607 s,
1608 " {} of {} merged run(s) recorded a bump ({:.0}%)",
1609 b.recorded,
1610 b.merged,
1611 b.coverage_rate()
1612 );
1613 let _ = writeln!(
1614 s,
1615 " {} release PR(s) opened, {} automerge enabled ({:.0}%), {} merged directly",
1616 b.pr_opened,
1617 b.automerge_enabled,
1618 b.automerge_rate(),
1619 b.merged_directly
1620 );
1621 let _ = writeln!(
1622 s,
1623 " {} clean, {} needed a human ({:.0}%)",
1624 b.clean(),
1625 b.needs_attention,
1626 b.attention_rate()
1627 );
1628 }
1629
1630 if stats.e2e.rounds > 0 || stats.e2e.deferred > 0 {
1631 let _ = writeln!(s, "\n{}", bold("verification"));
1632 let _ = writeln!(
1633 s,
1634 " {} rounds ran e2e, {} failed, {} of those with a clean static \
1635 review ({:.0}% sole detections), {} round(s) deferred it to the fixer",
1636 stats.e2e.rounds,
1637 stats.e2e.failures,
1638 stats.e2e.sole_detections,
1639 stats.e2e.sole_rate(),
1640 stats.e2e.deferred
1641 );
1642 }
1643
1644 if !stats.nodes.is_empty() {
1645 let _ = writeln!(
1646 s,
1647 "\n{}",
1648 bold("node durations (first to last event, per run)")
1649 );
1650 let max_mean = stats
1651 .nodes
1652 .iter()
1653 .map(|n| n.mean_secs())
1654 .fold(0.0_f64, f64::max);
1655 for n in &stats.nodes {
1656 let unmeasured = if n.single > 0 {
1657 format!(" {}", dim(&format!("{} unmeasured", n.single)))
1658 } else {
1659 String::new()
1660 };
1661 let _ = writeln!(
1662 s,
1663 " {:<14}{:>6} runs{:>8} mean{:>8} max {}{unmeasured}",
1664 n.node,
1665 n.runs,
1666 fmt_secs(n.mean_secs().round() as i64),
1667 fmt_secs(n.max_secs),
1668 bar(n.mean_secs(), max_mean, 20)
1669 );
1670 }
1671 }
1672 s
1673}
1674
1675#[cfg(test)]
1676mod tests {
1677 use super::*;
1678 use crate::config::Config;
1679 use crate::run::{
1680 Candidate, CommandOutcome, FixRecord, MergeOutcome, ReviewRecord, ReviewRound, RunState,
1681 Tally,
1682 };
1683 use std::collections::BTreeMap;
1684 use std::path::PathBuf;
1685 use std::sync::{Mutex, MutexGuard};
1686
1687 static SERIAL: Mutex<()> = Mutex::new(());
1689
1690 fn plain() -> MutexGuard<'static, ()> {
1691 let guard = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
1692 set_color(false);
1693 guard
1694 }
1695
1696 fn state() -> RunState {
1697 crate::run::pin_test_home();
1702 let mut s = RunState::new(
1703 PathBuf::from("/repo"),
1704 "main".to_owned(),
1705 "abcdef1234".to_owned(),
1706 "add retries to the uploader".to_owned(),
1707 Config::default(),
1708 );
1709 s.candidates = vec![Candidate {
1710 index: 0,
1711 label: 'A',
1712 agent: "opus".to_owned(),
1713 branch: "magi/x/A".to_owned(),
1714 worktree: PathBuf::from("/wt/A"),
1715 summary: String::new(),
1716 stat: String::new(),
1717 files: 3,
1718 commits: 2,
1719 empty: false,
1720 failed: None,
1721 verified_noop: None,
1722 duration_ms: 42_000,
1723 folded: false,
1724 }];
1725 s.tally = Some(Tally {
1726 first_choice: BTreeMap::from([('A', 3)]),
1727 borda: BTreeMap::new(),
1728 winner: 'A',
1729 rankings: 3,
1730 unanimous_initial: true,
1731 deliberated: false,
1732 changed_votes: 0,
1733 unanimous_final: true,
1734 tie_break: None,
1735 judges: 3,
1736 present: 3,
1737 quorum: 2,
1738 met_quorum: true,
1739 uncontested: None,
1740 });
1741 s
1742 }
1743
1744 #[test]
1745 fn run_report_names_the_winner_and_its_author() {
1746 let _guard = plain();
1747 let text = run(&state());
1748 assert!(text.contains("<- winner"), "{text}");
1749 assert!(text.contains("opus"));
1750 assert!(text.contains("3 files, 2 commits"));
1751 assert!(text.contains("winner A"));
1752 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
1753 }
1754
1755 #[test]
1756 fn a_candidates_own_summary_is_surfaced_not_only_kept_in_run_json() {
1757 let _guard = plain();
1763 let mut s = state();
1764 s.candidates[0].summary =
1765 "investigated 6c5e/8df3: both already merged, see talk 07fe.\nmore detail below."
1766 .to_owned();
1767 let text = run(&s);
1768 assert!(
1769 text.contains("investigated 6c5e/8df3: both already merged, see talk 07fe."),
1770 "{text}"
1771 );
1772 }
1773
1774 #[test]
1775 fn a_verified_noop_run_does_not_read_as_a_failure() {
1776 let _guard = plain();
1780 let mut s = state();
1781 s.status = RunStatus::VerifiedNoop;
1782 s.tally = None;
1783 s.candidates = vec![Candidate {
1784 index: 0,
1785 label: 'A',
1786 agent: "opus".to_owned(),
1787 branch: "magi/x/A".to_owned(),
1788 worktree: PathBuf::from("/wt/A"),
1789 summary: String::new(),
1790 stat: String::new(),
1791 files: 0,
1792 commits: 0,
1793 empty: true,
1794 failed: None,
1795 verified_noop: Some("already fixed by b32cfc4, which is on main".to_owned()),
1796 duration_ms: 9_000,
1797 folded: false,
1798 }];
1799 let text = run(&s);
1800 assert!(
1801 text.contains("agent-verified no-op"),
1802 "the status and the candidate flag must both say so: {text}"
1803 );
1804 assert!(
1805 text.contains("already fixed by b32cfc4"),
1806 "the evidence itself must be readable, not just the verdict: {text}"
1807 );
1808 assert!(
1809 !text.to_lowercase().contains("failed"),
1810 "a verified no-op must never read as the failure it is not: {text}"
1811 );
1812 }
1813
1814 #[test]
1815 fn colour_is_emitted_only_when_enabled() {
1816 let _guard = plain();
1817 set_color(true);
1818 let coloured = run(&state());
1819 set_color(false);
1820 let plain = run(&state());
1821 assert!(coloured.contains('\x1b'));
1822 assert!(!plain.contains('\x1b'));
1823 assert!(coloured.len() > plain.len());
1824 }
1825
1826 #[test]
1827 fn list_line_is_single_line() {
1828 let _guard = plain();
1829 let l = line(&state());
1830 assert_eq!(l.lines().count(), 1);
1831 assert!(l.contains("add retries"));
1832 assert!(l.contains("win A (opus)"));
1833 }
1834
1835 #[test]
1836 fn an_uncontested_run_does_not_read_as_a_collapsed_panel() {
1837 let _guard = plain();
1838 let mut s = state();
1839 s.tally = Some(Tally {
1840 first_choice: BTreeMap::from([('A', 0)]),
1841 borda: BTreeMap::new(),
1842 winner: 'A',
1843 rankings: 0,
1844 unanimous_initial: false,
1845 deliberated: false,
1846 changed_votes: 0,
1847 unanimous_final: false,
1848 tie_break: None,
1849 judges: 0,
1850 present: 0,
1851 quorum: 0,
1852 met_quorum: true,
1853 uncontested: Some(
1854 "only candidate A produced a usable change; no panel was asked".to_owned(),
1855 ),
1856 });
1857 let text = run(&s);
1858 assert!(
1859 !text.contains("0/3"),
1860 "no panel sat, so the judges line must not read as one that collapsed: {text}"
1861 );
1862 assert!(!text.contains("no usable ranking"), "{text}");
1863 assert!(!text.contains("still split"), "{text}");
1864 assert!(!text.contains("BELOW QUORUM"), "{text}");
1865 assert!(
1866 text.contains("not needed"),
1867 "the report must say judging was skipped, not silent: {text}"
1868 );
1869 assert!(text.contains("winner A"));
1870 }
1871
1872 #[test]
1873 fn a_below_quorum_run_still_reads_as_a_collapsed_panel() {
1874 let _guard = plain();
1875 let mut s = state();
1876 s.tally = Some(Tally {
1877 first_choice: BTreeMap::from([('A', 1), ('B', 0)]),
1878 borda: BTreeMap::new(),
1879 winner: 'A',
1880 rankings: 1,
1881 unanimous_initial: false,
1882 deliberated: false,
1883 changed_votes: 0,
1884 unanimous_final: false,
1885 tie_break: None,
1886 judges: 3,
1887 present: 1,
1888 quorum: 2,
1889 met_quorum: false,
1890 uncontested: None,
1891 });
1892 let text = run(&s);
1893 assert!(text.contains("1/3"), "{text}");
1894 assert!(
1895 text.contains("BELOW QUORUM"),
1896 "a real collapse must still be flagged: {text}"
1897 );
1898 assert!(
1899 !text.contains("not needed"),
1900 "a collapsed panel must not be described as one that was never asked: {text}"
1901 );
1902 }
1903
1904 #[test]
1905 fn a_mode_none_merge_does_not_read_as_landed() {
1906 let _guard = plain();
1907 let mut s = state();
1908 s.merge = Some(MergeOutcome {
1909 mode: crate::config::MergeMode::None,
1910 ok: true,
1911 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1912 empty: false,
1913 });
1914 let text = run(&s);
1915 assert!(
1916 !text.contains(" ok"),
1917 "mode none must not be shown as a landed merge: {text}"
1918 );
1919 assert!(text.contains("not landed"), "{text}");
1920 assert!(
1921 text.contains("branch magi/x/A"),
1922 "the report must say what's left behind: {text}"
1923 );
1924 assert!(
1925 text.contains("rebase"),
1926 "the report must point at the hand-landing steps: {text}"
1927 );
1928 assert!(
1929 !text.contains("placeholder subject"),
1930 "the default merge style is `merge`, which never inherits a \
1931 placeholder subject, so the squash caveat must not appear: {text}"
1932 );
1933 }
1934
1935 #[test]
1936 fn a_mode_none_squash_merge_warns_about_the_placeholder_subject() {
1937 let _guard = plain();
1938 let mut s = state();
1939 s.config.merge.style = MergeStyle::Squash;
1940 s.merge = Some(MergeOutcome {
1941 mode: crate::config::MergeMode::None,
1942 ok: true,
1943 detail: "git -C /repo merge --squash magi/x/A && git -C /repo commit -m \"add \
1944 retries\""
1945 .to_owned(),
1946 empty: false,
1947 });
1948 let text = run(&s);
1949 assert!(
1950 text.contains("placeholder subject"),
1951 "a squash-style manual merge must warn about the missing message: {text}"
1952 );
1953 assert!(text.contains("--squash"), "{text}");
1954 }
1955
1956 #[test]
1957 fn a_ready_run_left_by_merge_mode_none_does_not_read_as_a_plain_ready() {
1958 let _guard = plain();
1959 let mut s = state();
1960 s.status = RunStatus::Ready;
1961 s.merge = Some(MergeOutcome {
1962 mode: crate::config::MergeMode::None,
1963 ok: true,
1964 detail: "git -C /repo merge --no-ff magi/x/A".to_owned(),
1965 empty: false,
1966 });
1967
1968 let list = line(&s);
1969 assert!(
1970 !list.contains(" ready "),
1971 "a mode-none run must not read as a plain ready in `magi list`: {list}"
1972 );
1973 assert!(list.contains("no-op by design"), "{list}");
1974
1975 let full = run(&s);
1976 assert!(
1977 !full.contains("magi run") || !full.lines().next().unwrap().contains(" ready"),
1978 "the header line of `magi show` must not say plain ready either: {full}"
1979 );
1980 assert!(full.contains("no-op by design"), "{full}");
1981 }
1982
1983 #[test]
1984 fn an_ordinary_ready_run_still_reads_as_ready() {
1985 let _guard = plain();
1986 let mut s = state();
1987 s.status = RunStatus::Ready;
1988 s.merge = Some(MergeOutcome {
1992 mode: crate::config::MergeMode::Pr,
1993 ok: false,
1994 detail: "https://example.com/pr/1 was closed without merging".to_owned(),
1995 empty: false,
1996 });
1997
1998 let list = line(&s);
1999 assert!(list.contains("ready"), "{list}");
2000 assert!(!list.contains("no-op by design"), "{list}");
2001 }
2002
2003 #[test]
2004 fn the_list_line_does_not_flag_an_uncontested_run_as_short_judges() {
2005 let _guard = plain();
2006 let mut s = state();
2007 s.tally = Some(Tally {
2008 first_choice: BTreeMap::from([('A', 0)]),
2009 borda: BTreeMap::new(),
2010 winner: 'A',
2011 rankings: 0,
2012 unanimous_initial: false,
2013 deliberated: false,
2014 changed_votes: 0,
2015 unanimous_final: false,
2016 tie_break: None,
2017 judges: 0,
2018 present: 0,
2019 quorum: 0,
2020 met_quorum: true,
2021 uncontested: Some("only candidate A produced a usable change".to_owned()),
2022 });
2023 let l = line(&s);
2024 assert!(
2025 !l.contains("judges") && !l.contains("quorum"),
2026 "an uncontested run must not carry the same badge a short panel gets: {l}"
2027 );
2028 }
2029
2030 #[test]
2031 fn long_instructions_are_elided() {
2032 let _guard = plain();
2033 let mut s = state();
2034 s.instruction = "x".repeat(200);
2035 assert!(line(&s).contains('…'));
2036 }
2037
2038 #[test]
2039 fn a_lost_fix_report_reads_differently_from_zero_adoption() {
2040 let _guard = plain();
2041 let mut lost = state();
2042 lost.reviews = vec![ReviewRound {
2043 round: 1,
2044 head: "abc1234".to_owned(),
2045 verified_head: None,
2046 verified_at: None,
2047 reviews: Vec::new(),
2048 e2e: Vec::new(),
2049 verify_retried: false,
2050 e2e_deferred: false,
2051 e2e_defer_reason: None,
2052 fix: Some(FixRecord {
2053 agent: "opus".to_owned(),
2054 addressed: Vec::new(),
2055 rejected: Vec::new(),
2056 notes: String::new(),
2057 committed: true,
2058 failed: Some("timed out".to_owned()),
2059 duration_ms: 0,
2060 continuation: None,
2061 }),
2062 blocking: 3,
2063 answered: 0,
2064 expected: 0,
2065 clean: false,
2066 progressed: false,
2067 vote_split: false,
2068 reconsideration: Vec::new(),
2069 verdict: None,
2070 }];
2071 let text = run(&lost);
2072 assert!(text.contains("adoption report lost (timed out)"), "{text}");
2073 assert!(
2074 !text.contains("0 addressed"),
2075 "a lost report must never read as `0 addressed`: {text}"
2076 );
2077
2078 let mut rejected_all = state();
2079 rejected_all.reviews = vec![ReviewRound {
2080 round: 1,
2081 head: "abc1234".to_owned(),
2082 verified_head: None,
2083 verified_at: None,
2084 reviews: Vec::new(),
2085 e2e: Vec::new(),
2086 verify_retried: false,
2087 e2e_deferred: false,
2088 e2e_defer_reason: None,
2089 fix: Some(FixRecord {
2090 agent: "opus".to_owned(),
2091 addressed: Vec::new(),
2092 rejected: Vec::new(),
2093 notes: String::new(),
2094 committed: true,
2095 failed: None,
2096 duration_ms: 0,
2097 continuation: None,
2098 }),
2099 blocking: 3,
2100 answered: 0,
2101 expected: 0,
2102 clean: false,
2103 progressed: false,
2104 vote_split: false,
2105 reconsideration: Vec::new(),
2106 verdict: None,
2107 }];
2108 let text2 = run(&rejected_all);
2109 assert!(
2110 text2.contains("0 addressed / 0 rejected"),
2111 "a round the fixer actually reported on keeps the count: {text2}"
2112 );
2113 }
2114
2115 #[test]
2116 fn a_split_round_shows_every_seat_vote_and_the_reconsideration() {
2117 use crate::run::ReviewRevoteRecord;
2118 use crate::verdict::ReviewVote;
2119
2120 let _guard = plain();
2121 let mut s = state();
2122 s.reviews = vec![ReviewRound {
2123 round: 1,
2124 head: "abc1234".to_owned(),
2125 verified_head: None,
2126 verified_at: None,
2127 reviews: vec![
2128 ReviewRecord {
2129 attempts: 0,
2130 reviewer: 1,
2131 agent: "alpha".to_owned(),
2132 summary: String::new(),
2133 findings: Vec::new(),
2134 vote: Some(ReviewVote::Approve),
2135 failed: None,
2136 duration_ms: 0,
2137 },
2138 ReviewRecord {
2139 attempts: 0,
2140 reviewer: 2,
2141 agent: "beta".to_owned(),
2142 summary: String::new(),
2143 findings: Vec::new(),
2144 vote: Some(ReviewVote::Reject),
2145 failed: None,
2146 duration_ms: 0,
2147 },
2148 ],
2149 e2e: Vec::new(),
2150 verify_retried: false,
2151 e2e_deferred: false,
2152 e2e_defer_reason: None,
2153 fix: None,
2154 blocking: 0,
2155 answered: 2,
2156 expected: 2,
2157 clean: false,
2158 progressed: false,
2159 vote_split: true,
2160 reconsideration: vec![ReviewRevoteRecord {
2161 reviewer: 2,
2162 agent: "beta".to_owned(),
2163 vote: Some(ReviewVote::ApproveWithFindings),
2164 reason: "the other seat's read holds up".to_owned(),
2165 failed: None,
2166 }],
2167 verdict: Some(ReviewVote::ApproveWithFindings),
2168 }];
2169 let text = run(&s);
2170 assert!(text.contains("review-1 vote"), "{text}");
2171 assert!(text.contains("review-2 vote"), "{text}");
2172 assert!(text.contains("panel split"), "{text}");
2173 assert!(text.contains("reconsideration"), "{text}");
2174 assert!(text.contains("the other seat's read holds up"), "{text}");
2175 }
2176
2177 #[test]
2178 fn an_incomplete_panel_and_a_lost_fix_report_both_stay_on_the_round_line() {
2179 let _guard = plain();
2185 let mut s = state();
2186 s.reviews = vec![ReviewRound {
2187 round: 1,
2188 head: "abc1234".to_owned(),
2189 verified_head: None,
2190 verified_at: None,
2191 reviews: vec![
2192 ReviewRecord {
2193 attempts: 0,
2194 reviewer: 1,
2195 agent: "alpha".to_owned(),
2196 summary: String::new(),
2197 findings: Vec::new(),
2198 vote: None,
2199 failed: None,
2200 duration_ms: 0,
2201 },
2202 ReviewRecord {
2203 attempts: 0,
2204 reviewer: 2,
2205 agent: "beta".to_owned(),
2206 summary: String::new(),
2207 findings: Vec::new(),
2208 vote: None,
2209 failed: Some("agent timed out".to_owned()),
2210 duration_ms: 0,
2211 },
2212 ],
2213 e2e: Vec::new(),
2214 verify_retried: false,
2215 e2e_deferred: false,
2216 e2e_defer_reason: None,
2217 fix: Some(FixRecord {
2218 agent: "opus".to_owned(),
2219 addressed: Vec::new(),
2220 rejected: Vec::new(),
2221 notes: String::new(),
2222 committed: true,
2223 failed: Some("timed out".to_owned()),
2224 duration_ms: 0,
2225 continuation: None,
2226 }),
2227 blocking: 0,
2228 answered: 1,
2229 expected: 2,
2230 clean: false,
2231 progressed: true,
2232 vote_split: false,
2233 reconsideration: Vec::new(),
2234 verdict: None,
2235 }];
2236 let text = run(&s);
2237 assert!(text.contains("incomplete"), "{text}");
2238 assert!(text.contains("1/2 reviewers answered"), "{text}");
2239 assert!(text.contains("review-2: agent timed out"), "{text}");
2240 assert!(text.contains("adoption report lost (timed out)"), "{text}");
2241 assert!(
2242 !text.contains("clean"),
2243 "a round missing half its panel must never render as clean: {text}"
2244 );
2245 }
2246
2247 #[test]
2248 fn a_build_failure_is_not_reported_as_a_test_failure() {
2249 let _guard = plain();
2250 let mut s = state();
2251 s.reviews = vec![ReviewRound {
2252 round: 1,
2253 head: "abc1234".to_owned(),
2254 verified_head: None,
2255 verified_at: None,
2256 reviews: Vec::new(),
2257 e2e: vec![CommandOutcome {
2258 command: "cargo test".to_owned(),
2259 code: Some(1),
2260 output_tail: "LINK : fatal error LNK1104: cannot open file".to_owned(),
2261 duration_ms: 100,
2262 resource_blocked: false,
2263 }],
2264 verify_retried: true,
2265 e2e_deferred: false,
2266 e2e_defer_reason: None,
2267 fix: None,
2268 blocking: 0,
2269 answered: 0,
2270 expected: 0,
2271 clean: false,
2272 progressed: false,
2273 vote_split: false,
2274 reconsideration: Vec::new(),
2275 verdict: None,
2276 }];
2277 let text = run(&s);
2278 assert!(text.contains("could not run"), "{text}");
2279 assert!(text.contains("retried once"), "{text}");
2280 assert!(!text.contains("e2e RED"), "{text}");
2281 }
2282
2283 #[test]
2284 fn a_resource_blocked_e2e_never_reads_as_red_or_as_a_build_failure() {
2285 let _guard = plain();
2286 let mut s = state();
2287 s.reviews = vec![ReviewRound {
2288 round: 1,
2289 head: "abc1234".to_owned(),
2290 verified_head: None,
2291 verified_at: None,
2292 reviews: Vec::new(),
2293 e2e: vec![CommandOutcome {
2294 command: "(waiting for the shared build cache)".to_owned(),
2295 code: None,
2296 output_tail: "held by run x node e2e seat e2e".to_owned(),
2297 duration_ms: 100,
2298 resource_blocked: true,
2299 }],
2300 verify_retried: false,
2301 e2e_deferred: false,
2302 e2e_defer_reason: None,
2303 fix: None,
2304 blocking: 0,
2305 answered: 0,
2306 expected: 0,
2307 clean: false,
2308 progressed: false,
2309 vote_split: false,
2310 reconsideration: Vec::new(),
2311 verdict: None,
2312 }];
2313 let text = run(&s);
2314 assert!(text.contains("resource unavailable"), "{text}");
2315 assert!(!text.contains("e2e RED"), "{text}");
2316 assert!(!text.contains("build/link failure"), "{text}");
2317 }
2318
2319 #[test]
2320 fn a_round_with_more_than_one_e2e_command_names_each_one() {
2321 let _guard = plain();
2326 let mut s = state();
2327 s.reviews = vec![ReviewRound {
2328 round: 1,
2329 head: "abc1234".to_owned(),
2330 verified_head: Some("abc1234".to_owned()),
2331 verified_at: Some(jiff::Timestamp::now()),
2332 reviews: Vec::new(),
2333 e2e: vec![
2334 CommandOutcome {
2335 command: "cargo test --locked --all-targets".to_owned(),
2336 code: Some(0),
2337 output_tail: String::new(),
2338 duration_ms: 0,
2339 resource_blocked: false,
2340 },
2341 CommandOutcome {
2342 command: "cargo make check".to_owned(),
2343 code: Some(1),
2344 output_tail: "clippy: unused import".to_owned(),
2345 duration_ms: 0,
2346 resource_blocked: false,
2347 },
2348 ],
2349 verify_retried: false,
2350 e2e_deferred: false,
2351 e2e_defer_reason: None,
2352 fix: None,
2353 blocking: 0,
2354 answered: 0,
2355 expected: 0,
2356 clean: false,
2357 progressed: false,
2358 vote_split: false,
2359 reconsideration: Vec::new(),
2360 verdict: None,
2361 }];
2362 let text = run(&s);
2363 assert!(text.contains("cargo test --locked --all-targets"), "{text}");
2364 assert!(text.contains("cargo make check"), "{text}");
2365 assert!(text.contains("clippy: unused import"), "{text}");
2366 }
2367
2368 #[test]
2369 fn a_declined_finding_shows_its_reason() {
2370 use crate::verdict::{Finding, Rejection, Severity};
2371
2372 let _guard = plain();
2373 let mut s = state();
2374 s.status = RunStatus::Ready;
2375 s.reviews = vec![ReviewRound {
2376 round: 1,
2377 head: "deadbee".to_owned(),
2378 verified_head: None,
2379 verified_at: None,
2380 reviews: vec![ReviewRecord {
2381 attempts: 0,
2382 reviewer: 1,
2383 agent: "alpha".to_owned(),
2384 summary: String::new(),
2385 findings: vec![Finding {
2386 id: "R1-1-1".to_owned(),
2387 severity: Severity::Major,
2388 file: None,
2389 line: None,
2390 title: "still open".to_owned(),
2391 detail: String::new(),
2392 }],
2393 vote: None,
2394 failed: None,
2395 duration_ms: 0,
2396 }],
2397 e2e: vec![CommandOutcome {
2398 command: "cargo test".to_owned(),
2399 code: Some(0),
2400 output_tail: String::new(),
2401 duration_ms: 0,
2402 resource_blocked: false,
2403 }],
2404 verify_retried: false,
2405 e2e_deferred: false,
2406 e2e_defer_reason: None,
2407 fix: Some(FixRecord {
2408 agent: "alpha".to_owned(),
2409 addressed: Vec::new(),
2410 rejected: vec![Rejection {
2411 id: "R1-1-2".to_owned(),
2412 why: "cannot be triggered from any caller".to_owned(),
2413 }],
2414 notes: String::new(),
2415 committed: true,
2416 failed: None,
2417 duration_ms: 0,
2418 continuation: None,
2419 }),
2420 blocking: 1,
2421 answered: 1,
2422 expected: 1,
2423 clean: false,
2424 progressed: true,
2425 vote_split: false,
2426 reconsideration: Vec::new(),
2427 verdict: None,
2428 }];
2429
2430 let text = run(&s);
2431 assert!(text.contains("R1-1-2"), "{text}");
2432 assert!(text.contains("cannot be triggered"), "{text}");
2433 assert!(text.contains("still open"), "{text}");
2434 assert!(
2435 text.contains("handed off"),
2436 "a mergeable run with an open round must say so: {text}"
2437 );
2438 }
2439
2440 #[test]
2441 fn a_failing_gate_command_shows_its_output() {
2442 let _guard = plain();
2443 let mut s = state();
2444 s.status = RunStatus::Blocked;
2445 s.gate = vec![CommandOutcome {
2446 command: "cargo make check".to_owned(),
2447 code: Some(101),
2448 output_tail: "error[E0308]: mismatched types".to_owned(),
2449 duration_ms: 0,
2450 resource_blocked: false,
2451 }];
2452 s.gate_ran = true;
2453
2454 let text = run(&s);
2455 assert!(text.contains("mismatched types"), "{text}");
2456 }
2457
2458 #[test]
2459 fn a_gate_with_no_commands_is_reported_as_not_tested() {
2460 let _guard = plain();
2461 let mut s = state();
2462 s.status = RunStatus::Ready;
2463 s.gate_ran = true;
2464 assert!(s.gate.is_empty());
2465
2466 let text = run(&s);
2467 assert!(
2468 text.contains("gate")
2469 && text.contains("no gate commands configured")
2470 && text.contains("not tested"),
2471 "an unconfigured gate must be explicit: {text}"
2472 );
2473 }
2474
2475 #[test]
2476 fn a_gate_that_has_not_run_yet_shows_nothing() {
2477 let _guard = plain();
2478 let s = state();
2479 assert!(!s.gate_ran);
2480 assert!(s.gate.is_empty());
2481
2482 let text = run(&s);
2483 assert!(
2484 !text.contains("no gate commands configured"),
2485 "an unattempted gate must not be shown as a pass: {text}"
2486 );
2487 }
2488
2489 #[test]
2490 fn active_seats_shows_who_has_not_answered_and_how_long_is_left() {
2491 let _guard = plain();
2492 let mut s = state();
2493 s.seat_started("judge", "judge-2", std::time::Duration::from_secs(120), 0);
2494 let text = active_seats(&s, Liveness::Live);
2495 assert!(text.contains("running now"));
2496 assert!(text.contains("judge-2"));
2497 assert!(text.contains("judge"));
2498 assert!(!text.contains("no live daemon"), "{text}");
2499 assert!(!text.contains("could not be confirmed"), "{text}");
2500 }
2501
2502 #[test]
2503 fn active_seats_flags_a_leftover_from_a_dead_process() {
2504 let _guard = plain();
2505 let mut s = state();
2506 s.seat_started("implement", "impl-B", std::time::Duration::from_secs(60), 0);
2507 let text = active_seats(&s, Liveness::Dead);
2508 assert!(
2509 text.contains("no live daemon"),
2510 "a stale entry must not read as running: {text}"
2511 );
2512 }
2513
2514 #[test]
2515 fn superseded_notice_names_the_run_that_replaced_it() {
2516 let _guard = plain();
2517 let text = superseded_notice(Some("bbbb"));
2518 assert!(text.contains("SUPERSEDED"), "{text}");
2519 assert!(text.contains("bbbb"), "{text}");
2520 }
2521
2522 #[test]
2523 fn superseded_notice_is_silent_for_the_latest_attempt() {
2524 let _guard = plain();
2525 assert_eq!(superseded_notice(None), "");
2526 }
2527
2528 #[test]
2529 fn list_line_marks_a_nonterminal_run_with_a_dead_driver_stale() {
2530 let _guard = plain();
2531 let mut s = state();
2532 s.status = RunStatus::Reviewing;
2533 let text = line_with_liveness(&s, Liveness::Dead);
2534 assert!(text.contains("STALE"), "{text}");
2535 assert!(text.contains("resume required"), "{text}");
2536 assert!(liveness_notice(&s, Liveness::Dead).contains("STALE"));
2537 }
2538
2539 #[test]
2544 fn active_seats_reports_uncertainty_without_claiming_death() {
2545 let _guard = plain();
2546 let mut s = state();
2547 s.seat_started("review", "review-1", std::time::Duration::from_secs(60), 0);
2548 let text = active_seats(&s, Liveness::Unknown);
2549 assert!(
2550 text.contains("could not be confirmed"),
2551 "an unproven state must read as uncertain, not dead: {text}"
2552 );
2553 assert!(!text.contains("no live daemon"), "{text}");
2554 }
2555
2556 #[test]
2557 fn resource_block_reports_saved_evidence_in_text_and_web() {
2558 use crate::run::{ResourceBlock, ResourceBlockCause};
2559 let legacy = serde_json::json!({
2560 "node": "fix", "kind": "shared build cache",
2561 "detail": "the run is parking", "at": "2026-10-24T09:01:51Z"
2562 });
2563 let block: ResourceBlock = serde_json::from_value(legacy).unwrap();
2564 assert_eq!(block.cause, ResourceBlockCause::Unrecorded);
2565 for (cause, detail, expected) in [
2566 (
2567 ResourceBlockCause::Unrecorded,
2568 "the run is parking",
2569 "cancelled for parking",
2570 ),
2571 (ResourceBlockCause::Unrecorded, "busy", "cause unrecorded"),
2572 (
2573 ResourceBlockCause::Other,
2574 "metadata write denied",
2575 "operation blocked",
2576 ),
2577 (
2578 ResourceBlockCause::Timeout {
2579 waited_secs: 12,
2580 limit_secs: 10,
2581 },
2582 "busy",
2583 "timed out after 12s (limit 10s)",
2584 ),
2585 (
2586 ResourceBlockCause::Cancelled {
2587 park: Some(crate::run::ParkCause::Operator),
2588 },
2589 "cancelled",
2590 "cancelled for parking",
2591 ),
2592 ] {
2593 let mut state = state();
2594 state.resource_block = Some(ResourceBlock {
2595 cause: cause.clone(),
2596 detail: detail.into(),
2597 ..block.clone()
2598 });
2599 let text = active_seats(&state, Liveness::Dead);
2600 assert!(text.contains(expected), "{text}");
2601 assert!(
2602 !text.contains("no agent was started"),
2603 "earlier nodes may have run"
2604 );
2605 assert_eq!(
2606 crate::report_view::build_with(&state, Liveness::Dead, None).active_seats,
2607 text
2608 );
2609 let saved = serde_json::to_value(&state.resource_block).unwrap();
2610 let reloaded: ResourceBlock = serde_json::from_value(saved).unwrap();
2611 assert_eq!(reloaded.cause, cause);
2612 }
2613 }
2614
2615 #[test]
2616 fn active_seats_is_empty_when_nothing_is_running() {
2617 let _guard = plain();
2618 assert_eq!(active_seats(&state(), Liveness::Live), "");
2619 }
2620
2621 #[test]
2627 fn active_seats_shows_a_running_verify_task_and_its_command() {
2628 let _guard = plain();
2629 let mut s = state();
2630 s.task_command(
2631 "e2e",
2632 "verify",
2633 0,
2634 "cargo test",
2635 2,
2636 3,
2637 std::time::Duration::from_secs(600),
2638 );
2639 let text = active_seats(&s, Liveness::Live);
2640 assert!(text.contains("e2e"), "{text}");
2641 assert!(text.contains("(2/3)"), "{text}");
2642 assert!(text.contains("cargo test"), "{text}");
2643 }
2644
2645 #[test]
2646 fn no_jobs_section_appears_when_nothing_was_ever_collected() {
2647 let _guard = plain();
2648 assert!(!run(&state()).contains("background jobs"));
2651 }
2652
2653 #[test]
2654 fn a_codex_roster_with_no_completed_jobs_yet_says_so_instead_of_staying_silent() {
2655 let _guard = plain();
2656 let mut s = state();
2657 s.config.agents.push(crate::config::AgentSpec {
2658 id: "codex-one".to_owned(),
2659 kind: crate::config::AgentKind::Codex,
2660 model: None,
2661 command: vec!["codex".to_owned()],
2662 extra_args: Vec::new(),
2663 env: BTreeMap::new(),
2664 prompt_delivery: None,
2665 max_turn_secs: None,
2666 });
2667 let text = run(&s);
2668 assert!(
2669 text.contains("background jobs"),
2670 "a run that could report this must not read the same as one that never could: \
2671 {text}"
2672 );
2673 assert!(text.contains("no completed command evidence yet"));
2674 }
2675
2676 #[test]
2677 fn recovered_running_and_unreadable_jobs_are_told_apart() {
2678 let _guard = plain();
2679 let mut s = state();
2680 s.jobs = vec![
2681 crate::run::JobRecord {
2682 node: "implement".to_owned(),
2683 round: None,
2684 seat: "impl-A".to_owned(),
2685 id: "item49".to_owned(),
2686 description: "cargo test --test graph_cached_gate".to_owned(),
2687 checked_at: jiff::Timestamp::now(),
2688 status: crate::run::JobStatus::Completed,
2689 exit_code: Some(0),
2690 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2691 source: "codex".to_owned(),
2692 },
2693 crate::run::JobRecord {
2694 node: "fix".to_owned(),
2695 round: None,
2696 seat: "impl-A".to_owned(),
2697 id: "item52".to_owned(),
2698 description: "cargo test --test graph_split".to_owned(),
2699 checked_at: jiff::Timestamp::now(),
2700 status: crate::run::JobStatus::Failed,
2701 exit_code: Some(101),
2702 result_summary: "test result: 1 passed; 1 failed".to_owned(),
2703 source: "codex".to_owned(),
2704 },
2705 crate::run::JobRecord {
2706 node: "fix".to_owned(),
2707 round: None,
2708 seat: "impl-A".to_owned(),
2709 id: "item60".to_owned(),
2710 description: "cargo build".to_owned(),
2711 checked_at: jiff::Timestamp::now(),
2712 status: crate::run::JobStatus::Unknown,
2713 exit_code: None,
2714 result_summary: String::new(),
2715 source: "codex".to_owned(),
2716 },
2717 ];
2718 let text = run(&s);
2719 assert!(text.contains("background jobs"));
2720 assert!(text.contains("item49"));
2721 assert!(text.contains("item52"));
2722 assert!(text.contains("item60"));
2723 assert!(text.contains("completed"));
2727 assert!(text.contains("failed"));
2728 assert!(text.contains("unknown"));
2729 assert!(text.contains("adapter coverage"));
2731 }
2732
2733 #[test]
2734 fn a_jobs_own_round_is_shown_when_known() {
2735 let _guard = plain();
2736 let mut s = state();
2737 s.jobs = vec![crate::run::JobRecord {
2738 node: "review".to_owned(),
2739 round: Some(2),
2740 seat: "review-1".to_owned(),
2741 id: "item9".to_owned(),
2742 description: "cargo test --test graph_cached_gate".to_owned(),
2743 checked_at: jiff::Timestamp::now(),
2744 status: crate::run::JobStatus::Completed,
2745 exit_code: Some(0),
2746 result_summary: "test result: 2 passed; 0 failed".to_owned(),
2747 source: "codex".to_owned(),
2748 }];
2749 let text = run(&s);
2750 assert!(
2751 text.contains("round 2"),
2752 "the round this seat's own command ran in must be visible, distinct from magi's \
2753 own recorded verify: {text}"
2754 );
2755 }
2756
2757 #[test]
2758 fn stats_table_renders_without_runs() {
2759 let _guard = plain();
2760 let text = stats(&Stats::default());
2761 assert!(text.contains("0 total"));
2762 assert!(!text.contains("implementation"));
2763 assert!(!text.contains("node durations"));
2764 }
2765
2766 #[test]
2767 fn repo_summary_lists_run_count_and_completion_rate() {
2768 let _guard = plain();
2769 let a = crate::stats::RepoStats {
2770 repo: PathBuf::from("/repos/a"),
2771 name: "a".to_owned(),
2772 stats: Stats {
2773 totals: crate::stats::Totals {
2774 runs: 4,
2775 merged: 2,
2776 ready: 1,
2777 ..Default::default()
2778 },
2779 ..Default::default()
2780 },
2781 };
2782 let text = repo_summary(&[a]);
2783 assert!(text.contains("repos"), "{text}");
2784 assert!(text.contains("a 4 runs 75% completion"), "{text}");
2785 }
2786
2787 #[test]
2788 fn repo_summary_disambiguates_colliding_names_with_the_parent_directory() {
2789 let _guard = plain();
2790 let a = crate::stats::RepoStats {
2791 repo: PathBuf::from("/roots/one/magi"),
2792 name: "magi".to_owned(),
2793 stats: Stats::default(),
2794 };
2795 let b = crate::stats::RepoStats {
2796 repo: PathBuf::from("/roots/two/magi"),
2797 name: "magi".to_owned(),
2798 stats: Stats::default(),
2799 };
2800 let text = repo_summary(&[a, b]);
2801 assert!(text.contains("magi (one)"), "{text}");
2802 assert!(text.contains("magi (two)"), "{text}");
2803 }
2804
2805 #[test]
2806 fn stats_table_renders_advisor_reflection() {
2807 let _guard = plain();
2808 let st = Stats {
2809 advisors: vec![crate::stats::AdvisorStats {
2810 agent: "alpha".to_owned(),
2811 seated: 3,
2812 proposed: 2,
2813 absent: 1,
2814 faint: 1,
2815 strong: 1,
2816 }],
2817 ..Default::default()
2818 };
2819 let text = stats(&st);
2820 assert!(text.contains("design deliberation"));
2821 assert!(
2822 text.contains("approximate"),
2823 "must note the heuristic: {text}"
2824 );
2825 assert!(text.contains("alpha"));
2826 assert!(text.contains("50%"));
2827 }
2828
2829 #[test]
2830 fn stats_table_shows_release_bump_coverage_even_when_nothing_was_recorded() {
2831 let _guard = plain();
2836 let st = Stats {
2837 release_bumps: crate::stats::ReleaseBumpStats {
2838 merged: 3,
2839 ..Default::default()
2840 },
2841 ..Default::default()
2842 };
2843 let text = stats(&st);
2844 assert!(text.contains("release bumps"), "{text}");
2845 assert!(
2846 text.contains("0 of 3 merged run(s) recorded a bump"),
2847 "{text}"
2848 );
2849 }
2850
2851 #[test]
2852 fn stats_table_renders_release_bump_coverage_and_attention() {
2853 let _guard = plain();
2854 let st = Stats {
2855 release_bumps: crate::stats::ReleaseBumpStats {
2856 merged: 4,
2857 recorded: 2,
2858 pr_opened: 2,
2859 automerge_enabled: 1,
2860 merged_directly: 0,
2861 needs_attention: 1,
2862 },
2863 ..Default::default()
2864 };
2865 let text = stats(&st);
2866 assert!(text.contains("release bumps"), "{text}");
2867 assert!(
2868 text.contains("2 of 4 merged run(s) recorded a bump (50%)"),
2869 "{text}"
2870 );
2871 assert!(text.contains("1 clean, 1 needed a human (50%)"), "{text}");
2872 }
2873
2874 #[test]
2875 fn stats_table_renders_node_durations_with_a_bar_and_unmeasured_count() {
2876 let _guard = plain();
2877 let st = Stats {
2878 nodes: vec![
2879 crate::stats::NodeDuration {
2880 node: "implement".to_owned(),
2881 runs: 2,
2882 total_secs: 200,
2883 max_secs: 150,
2884 single: 1,
2885 },
2886 crate::stats::NodeDuration {
2887 node: "gate".to_owned(),
2888 runs: 0,
2889 total_secs: 0,
2890 max_secs: 0,
2891 single: 3,
2892 },
2893 ],
2894 ..Default::default()
2895 };
2896 let text = stats(&st);
2897 assert!(text.contains("node durations"));
2898 assert!(text.contains("implement"));
2899 assert!(text.contains("2 runs"));
2900 assert!(text.contains('█'), "the leader must draw a bar: {text}");
2901 assert!(text.contains("1 unmeasured"));
2902 assert!(text.contains("gate"));
2903 assert!(text.contains("3 unmeasured"));
2904 assert!(!text.contains('\x1b'), "colour leaked into a plain render");
2905 }
2906
2907 #[test]
2908 fn implementation_rates_use_independent_low_n_boundaries() {
2909 let _guard = plain();
2910 for (competitive_n, solo_n) in [(9, 10), (10, 9)] {
2911 let text = stats(&Stats {
2912 agents: vec![crate::stats::AgentStats {
2913 agent: "alpha".to_owned(),
2914 competitive_entered: competitive_n,
2915 competitive_wins: 1,
2916 solo_entered: solo_n,
2917 solo_adopted: 2,
2918 ..Default::default()
2919 }],
2920 ..Default::default()
2921 });
2922 let (competitive, solo) = text
2923 .split_once("implementation (solo runs, completion/adoption rate)")
2924 .unwrap();
2925 for (section, n, numerator) in [(competitive, competitive_n, 1), (solo, solo_n, 2)] {
2926 let row = section.lines().find(|line| line.contains("alpha")).unwrap();
2927 let cells: Vec<_> = row.split_whitespace().collect();
2928 assert_eq!(cells[1], numerator.to_string());
2929 assert_eq!(cells[2], n.to_string());
2930 assert_eq!(row.contains("low n"), n < 10);
2931 }
2932 }
2933 }
2934
2935 #[test]
2936 fn stats_table_renders_competitive_and_solo_tables_with_low_n() {
2937 let _guard = plain();
2938 let st = Stats {
2939 agents: vec![
2940 crate::stats::AgentStats {
2941 agent: "alpha".to_owned(),
2942 entered: 15,
2943 wins: 10,
2944 empty: 0,
2945 competitive_entered: 12,
2946 competitive_wins: 9,
2947 solo_entered: 3,
2948 solo_adopted: 1,
2949 },
2950 crate::stats::AgentStats {
2951 agent: "beta".to_owned(),
2952 entered: 5,
2953 wins: 2,
2954 empty: 1,
2955 competitive_entered: 4,
2956 competitive_wins: 2,
2957 solo_entered: 1,
2958 solo_adopted: 0,
2959 },
2960 ],
2961 ..Default::default()
2962 };
2963 let text = stats(&st);
2964 assert!(text.contains("implementation (competitive runs, 2+ candidates)"));
2965 assert!(text.contains("alpha"));
2966 assert!(text.contains("75%"));
2967 assert!(text.contains("beta"));
2968 assert!(text.contains("50%"));
2969 assert!(text.contains("implementation (solo runs, completion/adoption rate)"));
2970 assert!(text.contains("33%"));
2971 assert!(text.contains("low n"));
2972 }
2973}