ralph/commands/run/
dry_run.rs1use crate::agent::AgentOverrides;
16use crate::config;
17use crate::contracts::TaskStatus;
18use crate::queue;
19use crate::queue::RunnableSelectionOptions;
20use crate::queue::operations::queue_runnability_report;
21use anyhow::Result;
22
23use super::selection::select_run_one_task_index;
24
25pub fn dry_run_one(
33 resolved: &config::Resolved,
34 agent_overrides: &AgentOverrides,
35 target_task_id: Option<&str>,
36) -> Result<()> {
37 let queue_file = queue::load_queue(&resolved.queue_path)?;
39 let done = queue::load_queue_or_default(&resolved.done_path)?;
40 let done_ref = if done.tasks.is_empty() && !resolved.done_path.exists() {
41 None
42 } else {
43 Some(&done)
44 };
45
46 let include_draft = agent_overrides.include_draft.unwrap_or(false);
47
48 let max_depth = resolved.config.queue.max_dependency_depth.unwrap_or(10);
50 let warnings = queue::validate_queue_set(
51 &queue_file,
52 done_ref,
53 &resolved.id_prefix,
54 resolved.id_width,
55 max_depth,
56 )?;
57 queue::log_warnings(&warnings);
58
59 let selected = select_run_one_task_index(&queue_file, done_ref, target_task_id, include_draft)?;
61
62 if let Some(idx) = selected {
63 let task = &queue_file.tasks[idx];
64 println!("Dry run: would run {} (status: {:?})", task.id, task.status);
65
66 if task.status == TaskStatus::Doing {
68 println!(" Note: Task is already in 'doing' status (resuming).");
69 }
70 return Ok(());
71 }
72
73 println!("Dry run: no task would be run.");
75 println!();
76
77 let candidates: Vec<_> = queue_file
79 .tasks
80 .iter()
81 .filter(|t| {
82 t.status == TaskStatus::Todo || (include_draft && t.status == TaskStatus::Draft)
83 })
84 .collect();
85
86 if candidates.is_empty() {
87 if include_draft {
89 println!("No todo or draft tasks found.");
90 } else {
91 println!("No todo tasks found.");
92 }
93 return Ok(());
94 }
95
96 let options = RunnableSelectionOptions::new(include_draft, true);
98 match queue_runnability_report(&queue_file, done_ref, options) {
99 Ok(report) => {
100 if report.summary.runnable_candidates > 0 {
101 println!(
103 "Warning: runnability report found {} runnable candidates but selection returned none.",
104 report.summary.runnable_candidates
105 );
106 } else {
107 println!("Blockers preventing task execution:");
108
109 if report.summary.blocked_by_dependencies > 0 {
110 println!(
111 " - {} task(s) blocked by unmet dependencies",
112 report.summary.blocked_by_dependencies
113 );
114 }
115 if report.summary.blocked_by_schedule > 0 {
116 println!(
117 " - {} task(s) blocked by future schedule",
118 report.summary.blocked_by_schedule
119 );
120 }
121 if report.summary.blocked_by_status_or_flags > 0 {
122 println!(
123 " - {} task(s) blocked by status/flags (e.g., draft excluded)",
124 report.summary.blocked_by_status_or_flags
125 );
126 }
127
128 println!();
130 for row in &report.tasks {
131 let is_candidate = row.status == TaskStatus::Todo
132 || (include_draft && row.status == TaskStatus::Draft);
133 if !is_candidate || row.runnable || row.reasons.is_empty() {
134 continue;
135 }
136
137 println!("First blocking task: {} (status: {:?})", row.id, row.status);
138 for reason in &row.reasons {
139 match reason {
140 crate::queue::operations::NotRunnableReason::UnmetDependencies { dependencies } => {
141 if dependencies.len() == 1 {
142 match &dependencies[0] {
143 crate::queue::operations::DependencyIssue::Missing { id } => {
144 println!(" - Missing dependency: {}", id);
145 }
146 crate::queue::operations::DependencyIssue::NotComplete { id, status } => {
147 println!(" - Dependency {} not complete (status: {})", id, status);
148 }
149 }
150 } else {
151 println!(" - {} unmet dependencies", dependencies.len());
152 }
153 }
154 crate::queue::operations::NotRunnableReason::ScheduledStartInFuture { scheduled_start, seconds_until_runnable, .. } => {
155 let hours = seconds_until_runnable / 3600;
156 let minutes = (seconds_until_runnable % 3600) / 60;
157 if hours > 0 {
158 println!(" - Scheduled for: {} (in {}h {}m)",
159 scheduled_start, hours, minutes);
160 } else {
161 println!(" - Scheduled for: {} (in {}m)",
162 scheduled_start, minutes);
163 }
164 }
165 crate::queue::operations::NotRunnableReason::DraftExcluded => {
166 println!(" - Draft tasks excluded (use --include-draft)");
167 }
168 crate::queue::operations::NotRunnableReason::StatusNotRunnable { status } => {
169 println!(" - Status prevents running: {}", status);
170 }
171 }
172 }
173 break;
174 }
175 }
176 }
177 Err(e) => {
178 println!("Could not generate runnability report: {}", e);
179 }
180 }
181
182 println!();
183 println!("Run 'ralph queue explain' for a full report.");
184
185 Ok(())
186}
187
188pub fn dry_run_loop(resolved: &config::Resolved, agent_overrides: &AgentOverrides) -> Result<()> {
192 println!("Dry run: simulating run loop (reporting first selection only).");
193 println!("Note: Subsequent tasks depend on outcomes of earlier tasks.");
194 println!();
195
196 dry_run_one(resolved, agent_overrides, None)
197}