1use anyhow::Result;
2use apm_core::{classify_recovery_options, config::Config, is_merge_failure_state, ticket, RecoveryKind, RecoveryOption};
3use std::path::Path;
4
5pub fn run(root: &Path, json: bool, no_aggressive: bool) -> Result<()> {
6 let config = Config::load(root)?;
7 let aggressive = config.sync.aggressive && !no_aggressive;
8
9 crate::util::fetch_if_aggressive(root, aggressive);
10
11 let tickets = ticket::load_all_from_git(root, &config.tickets.dir)?;
12 let actionable_owned = config.actionable_states_for("agent");
13 let actionable: Vec<&str> = actionable_owned.iter().map(|s| s.as_str()).collect();
14 let p = &config.workflow.prioritization;
15 let caller_name = apm_core::config::resolve_caller_name();
16 let current_user = apm_core::config::resolve_identity(root);
17
18 match ticket::pick_next(&tickets, &actionable, &[], p.priority_weight, p.effort_weight, p.risk_weight, &config, Some(&caller_name), Some(¤t_user)) {
19 None => {
20 if json {
21 println!("null");
22 } else {
23 println!("No actionable tickets.");
24 }
25 }
26 Some(t) => {
27 let fm = &t.frontmatter;
28 if json {
29 println!(
30 r#"{{"id":{:?}, "title":{:?}, "state":{:?}, "score":{}}}"#,
31 fm.id, fm.title, fm.state, t.score(p.priority_weight, p.effort_weight, p.risk_weight)
32 );
33 } else {
34 println!("{} [{}] {}", fm.id, fm.state, fm.title);
35 if let Some(epic_id) = fm.epic.as_deref() {
36 if let Some(epic_branch) = apm_core::epic::find_epic_branch(root, epic_id) {
37 let s = apm_core::epic::merge_tree_status(root, &config.project.default_branch, &epic_branch)
38 .unwrap_or(apm_core::epic::MergeStatus { ahead: 0, clean: true });
39 let label = if s.ahead == 0 {
40 "up to date".to_string()
41 } else if s.clean {
42 format!("↓{} clean", s.ahead)
43 } else {
44 format!("↓{} CONFLICTS", s.ahead)
45 };
46 println!(" (epic {epic_id}: {label})");
47 }
48 }
49 if is_merge_failure_state(&fm.state, &config.workflow) {
50 let opts = classify_recovery_options(&fm.state, &config.workflow);
51 let mut groups: [Vec<&RecoveryOption>; 4] = [vec![], vec![], vec![], vec![]];
52 for opt in &opts {
53 let idx = match opt.kind {
54 RecoveryKind::RetryMerge => 0,
55 RecoveryKind::ReturnToWorker => 1,
56 RecoveryKind::Abandon => 2,
57 RecoveryKind::Other => 3,
58 };
59 groups[idx].push(opt);
60 }
61 println!("\nRecovery options:");
62 for opt in groups.iter().flatten() {
63 println!(" {} → apm state {} {}", opt.label, fm.id, opt.to);
64 }
65 println!();
66 }
67 }
68 }
69 }
70 Ok(())
71}