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