1use anyhow::Result;
2use colored::Colorize;
3use std::path::PathBuf;
4
5use crate::commands::helpers::{flatten_all_tasks, resolve_group_tag};
6use crate::models::task::{Task, TaskStatus};
7use crate::storage::Storage;
8
9pub enum NextTaskResult<'a> {
11 Available(&'a crate::models::task::Task),
13 NoPendingTasks,
15 BlockedByDependencies,
17}
18
19pub fn find_next_available<'a>(
22 phase: &'a crate::models::phase::Phase,
23 all_tasks: &[&Task],
24) -> NextTaskResult<'a> {
25 let pending_tasks: Vec<_> = phase
26 .tasks
27 .iter()
28 .filter(|t| t.status == TaskStatus::Pending)
29 .collect();
30
31 if pending_tasks.is_empty() {
32 return NextTaskResult::NoPendingTasks;
33 }
34
35 let deps_met: Vec<_> = pending_tasks
37 .iter()
38 .filter(|t| t.has_dependencies_met_refs(all_tasks))
39 .collect();
40
41 if deps_met.is_empty() {
42 return NextTaskResult::BlockedByDependencies;
43 }
44
45 NextTaskResult::Available(deps_met[0])
46}
47
48pub fn run(
49 project_root: Option<PathBuf>,
50 tag: Option<&str>,
51 spawn: bool,
52 all_tags: bool,
53) -> Result<()> {
54 let storage = Storage::new(project_root);
55 let tasks = storage.load_tasks()?;
56 let all_tasks_flat = flatten_all_tasks(&tasks);
57
58 if all_tags {
59 run_all_tags(&tasks, &all_tasks_flat, spawn)
61 } else {
62 let phase_tag = resolve_group_tag(&storage, tag, true)?;
64 let phase = tasks
65 .get(&phase_tag)
66 .ok_or_else(|| anyhow::anyhow!("Phase '{}' not found", phase_tag))?;
67
68 run_single_phase(phase, &phase_tag, &all_tasks_flat, spawn)
69 }
70}
71
72fn run_single_phase(
73 phase: &crate::models::phase::Phase,
74 phase_tag: &str,
75 all_tasks_flat: &[&Task],
76 spawn: bool,
77) -> Result<()> {
78 if spawn {
80 match find_next_available(phase, all_tasks_flat) {
81 NextTaskResult::Available(task) => {
82 let output = serde_json::json!({
83 "task_id": task.id,
84 "title": task.title,
85 "tag": phase_tag,
86 "complexity": task.complexity,
87 });
88 println!("{}", serde_json::to_string(&output)?);
89 }
90 _ => {
91 println!("null");
92 }
93 }
94 return Ok(());
95 }
96
97 match find_next_available(phase, all_tasks_flat) {
98 NextTaskResult::Available(task) => {
99 print_task_details(task);
100 print_standard_instructions(&task.id);
101 }
102 NextTaskResult::NoPendingTasks => {
103 println!("{}", "All tasks completed or in progress!".green().bold());
104 println!("Run: scud list --status in-progress");
105 }
106 NextTaskResult::BlockedByDependencies => {
107 println!(
108 "{}",
109 "No available tasks - all pending tasks blocked by dependencies".yellow()
110 );
111 println!("Run: scud list --status pending");
112 println!("Run: scud doctor # to diagnose stuck states");
113 }
114 }
115
116 Ok(())
117}
118
119fn run_all_tags(
120 all_phases: &std::collections::HashMap<String, crate::models::phase::Phase>,
121 all_tasks_flat: &[&Task],
122 spawn: bool,
123) -> Result<()> {
124 let mut pending_tasks: Vec<(&Task, &str)> = Vec::new();
126
127 for (tag, phase) in all_phases {
128 for task in &phase.tasks {
129 if task.status == TaskStatus::Pending {
131 if let Some(ref parent_id) = task.parent_id {
133 let parent_expanded = phase
134 .get_task(parent_id)
135 .map(|p| p.is_expanded())
136 .unwrap_or(false);
137 if parent_expanded {
138 pending_tasks.push((task, tag.as_str()));
139 }
140 } else if !task.is_expanded() {
141 pending_tasks.push((task, tag.as_str()));
143 }
144 }
145 }
146 }
147
148 let available = pending_tasks
150 .iter()
151 .find(|(task, _)| task.has_dependencies_met_refs(all_tasks_flat));
152
153 if spawn {
154 match available {
155 Some((task, tag)) => {
156 let output = serde_json::json!({
157 "task_id": task.id,
158 "title": task.title,
159 "tag": tag,
160 "complexity": task.complexity,
161 });
162 println!("{}", serde_json::to_string(&output)?);
163 }
164 None => {
165 println!("null");
166 }
167 }
168 return Ok(());
169 }
170
171 match available {
172 Some((task, tag)) => {
173 println!(
174 "{} {}",
175 "Phase:".dimmed(),
176 tag.cyan()
177 );
178 print_task_details(task);
179 print_standard_instructions(&task.id);
180 }
181 None => {
182 if pending_tasks.is_empty() {
183 println!("{}", "All tasks completed or in progress!".green().bold());
184 println!("Run: scud list --status in-progress");
185 } else {
186 println!(
187 "{}",
188 "No available tasks - all pending tasks blocked by dependencies".yellow()
189 );
190 println!("Pending tasks exist in {} phase(s), but all are blocked.", pending_tasks.len());
191 println!("Run: scud waves --all-tags # to see dependency graph");
192 println!("Run: scud doctor # to diagnose stuck states");
193 }
194 }
195 }
196
197 Ok(())
198}
199
200fn print_task_details(task: &crate::models::task::Task) {
201 println!("{}", "Next Available Task:".green().bold());
202 println!();
203 println!("{:<20} {}", "ID:".yellow(), task.id.cyan());
204 println!("{:<20} {}", "Title:".yellow(), task.title.bold());
205 println!("{:<20} {}", "Complexity:".yellow(), task.complexity);
206 println!("{:<20} {:?}", "Priority:".yellow(), task.priority);
207
208 if let Some(ref assigned) = task.assigned_to {
209 println!("{:<20} {}", "Assigned to:".yellow(), assigned.green());
210 }
211
212 println!();
213 println!("{}", "Description:".yellow());
214 println!("{}", task.description);
215
216 if let Some(details) = &task.details {
217 println!();
218 println!("{}", "Technical Details:".yellow());
219 println!("{}", details);
220 }
221
222 if let Some(test_strategy) = &task.test_strategy {
223 println!();
224 println!("{}", "Test Strategy:".yellow());
225 println!("{}", test_strategy);
226 }
227}
228
229fn print_standard_instructions(task_id: &str) {
230 println!();
231 println!("{}", "To start this task:".blue());
232 println!(" scud set-status {} in-progress", task_id);
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::models::phase::Phase;
239 use crate::models::task::{Task, TaskStatus};
240
241 fn create_test_phase() -> Phase {
242 let mut phase = Phase::new("test-phase".to_string());
243
244 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc 1".to_string());
245 task1.set_status(TaskStatus::Done);
246
247 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc 2".to_string());
248 task2.dependencies = vec!["1".to_string()];
249 let mut task3 = Task::new("3".to_string(), "Task 3".to_string(), "Desc 3".to_string());
252 task3.dependencies = vec!["2".to_string()];
253 phase.add_task(task1);
256 phase.add_task(task2);
257 phase.add_task(task3);
258
259 phase
260 }
261
262 fn get_task_refs(phase: &Phase) -> Vec<&Task> {
264 phase.tasks.iter().collect()
265 }
266
267 #[test]
268 fn test_find_next_available_basic() {
269 let phase = create_test_phase();
270 let all_tasks = get_task_refs(&phase);
271
272 match find_next_available(&phase, &all_tasks) {
273 NextTaskResult::Available(task) => {
274 assert_eq!(task.id, "2");
275 }
276 _ => panic!("Expected Available result"),
277 }
278 }
279
280 #[test]
281 fn test_find_next_no_pending() {
282 let mut phase = Phase::new("test".to_string());
283 let mut task = Task::new("1".to_string(), "Done".to_string(), "Desc".to_string());
284 task.set_status(TaskStatus::Done);
285 phase.add_task(task);
286
287 let all_tasks = get_task_refs(&phase);
288
289 match find_next_available(&phase, &all_tasks) {
290 NextTaskResult::NoPendingTasks => {}
291 _ => panic!("Expected NoPendingTasks result"),
292 }
293 }
294
295 #[test]
296 fn test_find_next_blocked_by_deps() {
297 let mut phase = Phase::new("test".to_string());
298
299 let task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
300 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc".to_string());
303 task2.dependencies = vec!["1".to_string()];
304 phase.add_task(task2);
308 phase.add_task(task1);
309
310 let all_tasks = get_task_refs(&phase);
311
312 match find_next_available(&phase, &all_tasks) {
314 NextTaskResult::Available(task) => {
315 assert_eq!(task.id, "1");
316 }
317 _ => panic!("Expected task 1 to be available"),
318 }
319 }
320
321 #[test]
322 fn test_find_next_all_blocked() {
323 let mut phase = Phase::new("test".to_string());
324
325 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
326 task1.dependencies = vec!["nonexistent".to_string()];
327 phase.add_task(task1);
330
331 let all_tasks = get_task_refs(&phase);
332
333 match find_next_available(&phase, &all_tasks) {
334 NextTaskResult::BlockedByDependencies => {}
335 _ => panic!("Expected BlockedByDependencies result"),
336 }
337 }
338
339 #[test]
340 fn test_find_next_cross_tag_dependency() {
341 let mut phase = Phase::new("api".to_string());
343 let mut api_task = Task::new(
344 "api:1".to_string(),
345 "API Task".to_string(),
346 "Desc".to_string(),
347 );
348 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
350
351 let mut auth_task = Task::new(
353 "auth:1".to_string(),
354 "Auth Task".to_string(),
355 "Desc".to_string(),
356 );
357 auth_task.set_status(TaskStatus::Done);
358
359 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
361
362 match find_next_available(&phase, &all_tasks) {
364 NextTaskResult::Available(task) => {
365 assert_eq!(task.id, "api:1");
366 }
367 _ => panic!("Expected Available result with cross-tag dependency met"),
368 }
369 }
370
371 #[test]
372 fn test_find_next_cross_tag_dependency_not_met() {
373 let mut phase = Phase::new("api".to_string());
375 let mut api_task = Task::new(
376 "api:1".to_string(),
377 "API Task".to_string(),
378 "Desc".to_string(),
379 );
380 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
382
383 let auth_task = Task::new(
385 "auth:1".to_string(),
386 "Auth Task".to_string(),
387 "Desc".to_string(),
388 );
389
390 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
392
393 match find_next_available(&phase, &all_tasks) {
395 NextTaskResult::BlockedByDependencies => {}
396 _ => panic!("Expected BlockedByDependencies with cross-tag dep not met"),
397 }
398 }
399}