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(project_root: Option<PathBuf>, tag: Option<&str>, spawn: bool) -> Result<()> {
49 let storage = Storage::new(project_root);
50 let phase_tag = resolve_group_tag(&storage, tag, true)?;
51
52 let tasks = storage.load_tasks()?;
54 let all_tasks_flat = flatten_all_tasks(&tasks);
55 let phase = tasks
56 .get(&phase_tag)
57 .ok_or_else(|| anyhow::anyhow!("Phase '{}' not found", phase_tag))?;
58
59 if spawn {
61 match find_next_available(phase, &all_tasks_flat) {
62 NextTaskResult::Available(task) => {
63 let output = serde_json::json!({
64 "task_id": task.id,
65 "title": task.title,
66 "tag": phase_tag,
67 "complexity": task.complexity,
68 });
69 println!("{}", serde_json::to_string(&output)?);
70 }
71 _ => {
72 println!("null");
73 }
74 }
75 return Ok(());
76 }
77
78 match find_next_available(phase, &all_tasks_flat) {
79 NextTaskResult::Available(task) => {
80 print_task_details(task);
81 print_standard_instructions(&task.id);
82 }
83 NextTaskResult::NoPendingTasks => {
84 println!("{}", "All tasks completed or in progress!".green().bold());
85 println!("Run: scud list --status in-progress");
86 }
87 NextTaskResult::BlockedByDependencies => {
88 println!(
89 "{}",
90 "No available tasks - all pending tasks blocked by dependencies".yellow()
91 );
92 println!("Run: scud list --status pending");
93 println!("Run: scud doctor # to diagnose stuck states");
94 }
95 }
96
97 Ok(())
98}
99
100fn print_task_details(task: &crate::models::task::Task) {
101 println!("{}", "Next Available Task:".green().bold());
102 println!();
103 println!("{:<20} {}", "ID:".yellow(), task.id.cyan());
104 println!("{:<20} {}", "Title:".yellow(), task.title.bold());
105 println!("{:<20} {}", "Complexity:".yellow(), task.complexity);
106 println!("{:<20} {:?}", "Priority:".yellow(), task.priority);
107
108 if let Some(ref assigned) = task.assigned_to {
109 println!("{:<20} {}", "Assigned to:".yellow(), assigned.green());
110 }
111
112 println!();
113 println!("{}", "Description:".yellow());
114 println!("{}", task.description);
115
116 if let Some(details) = &task.details {
117 println!();
118 println!("{}", "Technical Details:".yellow());
119 println!("{}", details);
120 }
121
122 if let Some(test_strategy) = &task.test_strategy {
123 println!();
124 println!("{}", "Test Strategy:".yellow());
125 println!("{}", test_strategy);
126 }
127}
128
129fn print_standard_instructions(task_id: &str) {
130 println!();
131 println!("{}", "To start this task:".blue());
132 println!(" scud set-status {} in-progress", task_id);
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::models::phase::Phase;
139 use crate::models::task::{Task, TaskStatus};
140
141 fn create_test_phase() -> Phase {
142 let mut phase = Phase::new("test-phase".to_string());
143
144 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc 1".to_string());
145 task1.set_status(TaskStatus::Done);
146
147 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc 2".to_string());
148 task2.dependencies = vec!["1".to_string()];
149 let mut task3 = Task::new("3".to_string(), "Task 3".to_string(), "Desc 3".to_string());
152 task3.dependencies = vec!["2".to_string()];
153 phase.add_task(task1);
156 phase.add_task(task2);
157 phase.add_task(task3);
158
159 phase
160 }
161
162 fn get_task_refs(phase: &Phase) -> Vec<&Task> {
164 phase.tasks.iter().collect()
165 }
166
167 #[test]
168 fn test_find_next_available_basic() {
169 let phase = create_test_phase();
170 let all_tasks = get_task_refs(&phase);
171
172 match find_next_available(&phase, &all_tasks) {
173 NextTaskResult::Available(task) => {
174 assert_eq!(task.id, "2");
175 }
176 _ => panic!("Expected Available result"),
177 }
178 }
179
180 #[test]
181 fn test_find_next_no_pending() {
182 let mut phase = Phase::new("test".to_string());
183 let mut task = Task::new("1".to_string(), "Done".to_string(), "Desc".to_string());
184 task.set_status(TaskStatus::Done);
185 phase.add_task(task);
186
187 let all_tasks = get_task_refs(&phase);
188
189 match find_next_available(&phase, &all_tasks) {
190 NextTaskResult::NoPendingTasks => {}
191 _ => panic!("Expected NoPendingTasks result"),
192 }
193 }
194
195 #[test]
196 fn test_find_next_blocked_by_deps() {
197 let mut phase = Phase::new("test".to_string());
198
199 let task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
200 let mut task2 = Task::new("2".to_string(), "Task 2".to_string(), "Desc".to_string());
203 task2.dependencies = vec!["1".to_string()];
204 phase.add_task(task2);
208 phase.add_task(task1);
209
210 let all_tasks = get_task_refs(&phase);
211
212 match find_next_available(&phase, &all_tasks) {
214 NextTaskResult::Available(task) => {
215 assert_eq!(task.id, "1");
216 }
217 _ => panic!("Expected task 1 to be available"),
218 }
219 }
220
221 #[test]
222 fn test_find_next_all_blocked() {
223 let mut phase = Phase::new("test".to_string());
224
225 let mut task1 = Task::new("1".to_string(), "Task 1".to_string(), "Desc".to_string());
226 task1.dependencies = vec!["nonexistent".to_string()];
227 phase.add_task(task1);
230
231 let all_tasks = get_task_refs(&phase);
232
233 match find_next_available(&phase, &all_tasks) {
234 NextTaskResult::BlockedByDependencies => {}
235 _ => panic!("Expected BlockedByDependencies result"),
236 }
237 }
238
239 #[test]
240 fn test_find_next_cross_tag_dependency() {
241 let mut phase = Phase::new("api".to_string());
243 let mut api_task = Task::new(
244 "api:1".to_string(),
245 "API Task".to_string(),
246 "Desc".to_string(),
247 );
248 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
250
251 let mut auth_task = Task::new(
253 "auth:1".to_string(),
254 "Auth Task".to_string(),
255 "Desc".to_string(),
256 );
257 auth_task.set_status(TaskStatus::Done);
258
259 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
261
262 match find_next_available(&phase, &all_tasks) {
264 NextTaskResult::Available(task) => {
265 assert_eq!(task.id, "api:1");
266 }
267 _ => panic!("Expected Available result with cross-tag dependency met"),
268 }
269 }
270
271 #[test]
272 fn test_find_next_cross_tag_dependency_not_met() {
273 let mut phase = Phase::new("api".to_string());
275 let mut api_task = Task::new(
276 "api:1".to_string(),
277 "API Task".to_string(),
278 "Desc".to_string(),
279 );
280 api_task.dependencies = vec!["auth:1".to_string()]; phase.add_task(api_task);
282
283 let auth_task = Task::new(
285 "auth:1".to_string(),
286 "Auth Task".to_string(),
287 "Desc".to_string(),
288 );
289
290 let all_tasks: Vec<&Task> = vec![&phase.tasks[0], &auth_task];
292
293 match find_next_available(&phase, &all_tasks) {
295 NextTaskResult::BlockedByDependencies => {}
296 _ => panic!("Expected BlockedByDependencies with cross-tag dep not met"),
297 }
298 }
299}