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!("{} {}", "Phase:".dimmed(), tag.cyan());
174 print_task_details(task);
175 print_standard_instructions(&task.id);
176 }
177 None => {
178 if pending_tasks.is_empty() {
179 println!("{}", "All tasks completed or in progress!".green().bold());
180 println!("Run: scud list --status in-progress");
181 } else {
182 println!(
183 "{}",
184 "No available tasks - all pending tasks blocked by dependencies".yellow()
185 );
186 println!(
187 "Pending tasks exist in {} phase(s), but all are blocked.",
188 pending_tasks.len()
189 );
190 println!("Run: scud waves --all-tags # to see dependency graph");
191 println!("Run: scud doctor # to diagnose stuck states");
192 }
193 }
194 }
195
196 Ok(())
197}
198
199fn print_task_details(task: &crate::models::task::Task) {
200 println!("{}", "Next Available Task:".green().bold());
201 println!();
202 println!("{:<20} {}", "ID:".yellow(), task.id.cyan());
203 println!("{:<20} {}", "Title:".yellow(), task.title.bold());
204 println!("{:<20} {}", "Complexity:".yellow(), task.complexity);
205 println!("{:<20} {:?}", "Priority:".yellow(), task.priority);
206
207 if let Some(ref assigned) = task.assigned_to {
208 println!("{:<20} {}", "Assigned to:".yellow(), assigned.green());
209 }
210
211 println!();
212 println!("{}", "Description:".yellow());
213 println!("{}", task.description);
214
215 if let Some(details) = &task.details {
216 println!();
217 println!("{}", "Technical Details:".yellow());
218 println!("{}", details);
219 }
220
221 if let Some(test_strategy) = &task.test_strategy {
222 println!();
223 println!("{}", "Test Strategy:".yellow());
224 println!("{}", test_strategy);
225 }
226}
227
228fn print_standard_instructions(task_id: &str) {
229 println!();
230 println!("{}", "To start this task:".blue());
231 println!(" scud set-status {} in-progress", task_id);
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::models::phase::Phase;
238 use crate::models::task::{Task, TaskStatus};
239
240 fn create_test_phase() -> Phase {
241 let mut phase = Phase::new("test-phase".to_string());
242
243 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc 1".to_string());
244 task1.set_status(TaskStatus::Done);
245
246 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc 2".to_string());
247 task2.dependencies = vec!["1".to_string()];
248 let mut task3 = Task::new("3".to_string(), "Task 3".to_string(), "Desc 3".to_string());
251 task3.dependencies = vec!["2".to_string()];
252 phase.add_task(task1);
255 phase.add_task(task2);
256 phase.add_task(task3);
257
258 phase
259 }
260
261 fn get_task_refs(phase: &Phase) -> Vec<&Task> {
263 phase.tasks.iter().collect()
264 }
265
266 #[test]
267 fn test_find_next_available_basic() {
268 let phase = create_test_phase();
269 let all_tasks = get_task_refs(&phase);
270
271 match find_next_available(&phase, &all_tasks) {
272 NextTaskResult::Available(task) => {
273 assert_eq!(task.id, "2");
274 }
275 _ => panic!("Expected Available result"),
276 }
277 }
278
279 #[test]
280 fn test_find_next_no_pending() {
281 let mut phase = Phase::new("test".to_string());
282 let mut task = Task::new("1".to_string(), "Done".to_string(), "Desc".to_string());
283 task.set_status(TaskStatus::Done);
284 phase.add_task(task);
285
286 let all_tasks = get_task_refs(&phase);
287
288 match find_next_available(&phase, &all_tasks) {
289 NextTaskResult::NoPendingTasks => {}
290 _ => panic!("Expected NoPendingTasks result"),
291 }
292 }
293
294 #[test]
295 fn test_find_next_blocked_by_deps() {
296 let mut phase = Phase::new("test".to_string());
297
298 let task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
299 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc".to_string());
302 task2.dependencies = vec!["1".to_string()];
303 phase.add_task(task2);
307 phase.add_task(task1);
308
309 let all_tasks = get_task_refs(&phase);
310
311 match find_next_available(&phase, &all_tasks) {
313 NextTaskResult::Available(task) => {
314 assert_eq!(task.id, "1");
315 }
316 _ => panic!("Expected task 1 to be available"),
317 }
318 }
319
320 #[test]
321 fn test_find_next_all_blocked() {
322 let mut phase = Phase::new("test".to_string());
323
324 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
325 task1.dependencies = vec!["nonexistent".to_string()];
326 phase.add_task(task1);
329
330 let all_tasks = get_task_refs(&phase);
331
332 match find_next_available(&phase, &all_tasks) {
333 NextTaskResult::BlockedByDependencies => {}
334 _ => panic!("Expected BlockedByDependencies result"),
335 }
336 }
337
338 #[test]
339 fn test_find_next_cross_tag_dependency() {
340 let mut phase = Phase::new("api".to_string());
342 let mut api_task = Task::new(
343 "api:1".to_string(),
344 "API Task".to_string(),
345 "Desc".to_string(),
346 );
347 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
349
350 let mut auth_task = Task::new(
352 "auth:1".to_string(),
353 "Auth Task".to_string(),
354 "Desc".to_string(),
355 );
356 auth_task.set_status(TaskStatus::Done);
357
358 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
360
361 match find_next_available(&phase, &all_tasks) {
363 NextTaskResult::Available(task) => {
364 assert_eq!(task.id, "api:1");
365 }
366 _ => panic!("Expected Available result with cross-tag dependency met"),
367 }
368 }
369
370 #[test]
371 fn test_find_next_cross_tag_dependency_not_met() {
372 let mut phase = Phase::new("api".to_string());
374 let mut api_task = Task::new(
375 "api:1".to_string(),
376 "API Task".to_string(),
377 "Desc".to_string(),
378 );
379 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
381
382 let auth_task = Task::new(
384 "auth:1".to_string(),
385 "Auth Task".to_string(),
386 "Desc".to_string(),
387 );
388
389 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
391
392 match find_next_available(&phase, &all_tasks) {
394 NextTaskResult::BlockedByDependencies => {}
395 _ => panic!("Expected BlockedByDependencies with cross-tag dep not met"),
396 }
397 }
398}