use crate::contracts::{BlockingState, QueueFile, Task, TaskStatus};
use anyhow::Result;
use std::path::Path;
use super::orchestration::SelectTaskResult;
use crate::commands::run::{
RunEventHandler, emit_resume_decision,
run_session::{ResumeTaskValidation, validate_resumed_task},
selection::select_run_one_task_index,
};
use crate::queue::{RunnableSelectionOptions, operations::QueueRunnabilitySummary};
pub(crate) fn select_task_for_run(
queue_file: &QueueFile,
done_ref: Option<&QueueFile>,
target_task_id: Option<&str>,
resume_task_id: Option<&str>,
repo_root: &Path,
include_draft: bool,
run_event_handler: Option<&RunEventHandler>,
) -> Result<SelectTaskResult> {
let effective_target = if target_task_id.is_some() {
target_task_id
} else if let Some(resume_id) = resume_task_id {
match validate_resumed_task(queue_file, resume_id, repo_root)? {
ResumeTaskValidation::Resumable => Some(resume_id),
ResumeTaskValidation::FreshStart(decision) => {
emit_resume_decision(&decision, run_event_handler);
None
}
}
} else {
None
};
let task_idx =
match select_run_one_task_index(queue_file, done_ref, effective_target, include_draft)? {
Some(idx) => idx,
None => {
let candidates: Vec<_> = queue_file
.tasks
.iter()
.filter(|t| {
t.is_executable_work_item()
&& (t.status == TaskStatus::Todo
|| t.status == TaskStatus::Doing
|| (include_draft && t.status == TaskStatus::Draft))
})
.cloned()
.collect();
if candidates.is_empty() {
if include_draft {
log::info!("No todo or draft tasks found.");
} else {
log::info!("No todo tasks found.");
}
return Ok(SelectTaskResult::NoCandidates);
}
let summary =
build_blocked_summary(queue_file, done_ref, &candidates, include_draft);
let state = summary.blocking.clone().unwrap_or_else(|| {
BlockingState::idle(include_draft)
.with_observed_at(crate::timeutil::now_utc_rfc3339_or_fallback())
});
return Ok(SelectTaskResult::Blocked {
summary: Box::new(summary),
state: Box::new(state),
});
}
};
let task = queue_file.tasks[task_idx].clone();
Ok(SelectTaskResult::Selected {
task: Box::new(task),
})
}
fn build_blocked_summary(
queue_file: &QueueFile,
done_ref: Option<&QueueFile>,
candidates: &[Task],
include_draft: bool,
) -> QueueRunnabilitySummary {
let options = RunnableSelectionOptions::new(include_draft, true);
match crate::queue::operations::queue_runnability_report(queue_file, done_ref, options) {
Ok(report) => {
if let Some(blocking) = report.summary.blocking.as_ref() {
log::info!("{}", blocking.message);
if !blocking.detail.trim().is_empty() {
log::info!(" {}", blocking.detail);
}
} else {
log::info!("All runnable tasks are blocked.");
}
log::info!("Run 'cueloop queue explain' for details.");
report.summary.clone()
}
Err(e) => {
log::info!(
"No runnable tasks found (failed to analyze blockers: {}).",
e
);
log::info!("Run 'cueloop queue explain' for details.");
QueueRunnabilitySummary {
total_active: queue_file.tasks.len(),
candidates_total: candidates.len(),
runnable_candidates: 0,
blocked_by_dependencies: candidates.len(),
blocked_by_schedule: 0,
blocked_by_status_or_flags: 0,
blocking: Some(
BlockingState::dependency_blocked(candidates.len())
.with_observed_at(crate::timeutil::now_utc_rfc3339_or_fallback()),
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::{TaskKind, TaskPriority};
use std::collections::HashMap;
use std::path::Path;
fn task(id: &str, status: TaskStatus) -> Task {
Task {
id: id.to_string(),
status,
kind: TaskKind::WorkItem,
title: format!("Task {id}"),
description: None,
priority: TaskPriority::Medium,
tags: vec![],
scope: vec![],
evidence: vec![],
plan: vec![],
notes: vec![],
request: None,
agent: None,
created_at: Some("2026-01-18T00:00:00Z".to_string()),
updated_at: Some("2026-01-18T00:00:00Z".to_string()),
completed_at: None,
started_at: None,
scheduled_start: None,
estimated_minutes: None,
actual_minutes: None,
depends_on: vec![],
blocks: vec![],
relates_to: vec![],
duplicates: None,
custom_fields: HashMap::new(),
parent_id: None,
}
}
#[test]
fn select_task_for_run_reports_blocked_doing_candidate() -> anyhow::Result<()> {
let mut doing = task("CL-0001", TaskStatus::Doing);
doing.depends_on = vec!["CL-9999".to_string()];
let queue = QueueFile {
version: 1,
tasks: vec![doing],
};
let result = select_task_for_run(&queue, None, None, None, Path::new("."), false, None)?;
match result {
SelectTaskResult::Blocked { summary, .. } => {
assert_eq!(summary.candidates_total, 1);
assert_eq!(summary.blocked_by_dependencies, 1);
}
SelectTaskResult::Selected { .. } => panic!("expected blocked selection, got selected"),
SelectTaskResult::NoCandidates => {
panic!("expected blocked selection, got no candidates")
}
}
Ok(())
}
}