use anyhow::{Result, bail};
use crate::agent;
use crate::cli::task::args::{
TaskDecomposeArgs, TaskDecomposeChildPolicyArg, TaskDecomposeFormatArg,
};
use crate::commands::task as task_cmd;
use crate::config;
use crate::contracts::MachineDecomposeDocument;
pub fn handle(args: &TaskDecomposeArgs, force: bool, resolved: &config::Resolved) -> Result<()> {
if let Some(checkpoint_id) = args.from_preview.as_deref() {
validate_from_preview_args(args)?;
let (preview, checkpoint) =
task_cmd::load_decomposition_preview_checkpoint(resolved, checkpoint_id)?;
let write_result = Some(task_cmd::write_task_decomposition(
resolved, &preview, force,
)?);
let document = crate::cli::machine::build_task_decompose_document(
&preview,
write_result.as_ref(),
Some(&checkpoint),
);
match args.format {
TaskDecomposeFormatArg::Text => {
print_text_output(&preview, write_result.as_ref(), &document)
}
TaskDecomposeFormatArg::Json => {
println!("{}", serde_json::to_string_pretty(&document)?)
}
}
return Ok(());
}
let source = source_from_args(resolved, &args.source, args.from_file.as_deref())?;
let overrides = agent::resolve_agent_overrides(&agent::AgentArgs {
runner: args.runner.clone(),
model: args.model.clone(),
effort: args.effort.clone(),
repo_prompt: args.repo_prompt,
runner_cli: args.runner_cli.clone(),
})?;
let status = args.status.into();
let parent_status = args.parent_status.map(Into::into).unwrap_or(status);
let leaf_status = args.leaf_status.map(Into::into).unwrap_or(status);
let preview = task_cmd::plan_task_decomposition(
resolved,
&task_cmd::TaskDecomposeOptions {
source,
attach_to_task_id: args.attach_to.clone(),
max_depth: args.max_depth,
max_children: usize::from(args.max_children),
max_nodes: usize::from(args.max_nodes),
status,
parent_status,
leaf_status,
child_policy: child_policy(args.child_policy),
with_dependencies: args.with_dependencies,
runner_override: overrides.runner,
model_override: overrides.model,
reasoning_effort_override: overrides.reasoning_effort,
runner_cli_overrides: overrides.runner_cli,
repoprompt_tool_injection: agent::resolve_rp_required(args.repo_prompt, resolved),
stream_planner_output: args.format == TaskDecomposeFormatArg::Text,
force,
},
)?;
let write_result = if args.write {
Some(task_cmd::write_task_decomposition(
resolved, &preview, force,
)?)
} else {
None
};
let checkpoint = if args.write {
None
} else {
Some(task_cmd::save_decomposition_preview_checkpoint(
resolved, &preview,
)?)
};
let document = crate::cli::machine::build_task_decompose_document(
&preview,
write_result.as_ref(),
checkpoint.as_ref(),
);
match args.format {
TaskDecomposeFormatArg::Text => {
print_text_output(&preview, write_result.as_ref(), &document)
}
TaskDecomposeFormatArg::Json => println!("{}", serde_json::to_string_pretty(&document)?),
}
Ok(())
}
fn validate_from_preview_args(args: &TaskDecomposeArgs) -> Result<()> {
if !args.write {
bail!("`cueloop task decompose --from-preview` requires --write for queue mutation.");
}
if args.preview {
bail!("`cueloop task decompose --from-preview` cannot be combined with --preview.");
}
if !args.source.is_empty() || args.from_file.is_some() {
bail!(
"`cueloop task decompose --from-preview` cannot be combined with SOURCE text or --from-file."
);
}
if args.attach_to.is_some()
|| args.with_dependencies
|| args.max_depth != 3
|| args.max_children != 5
|| args.max_nodes != 50
|| args.status != crate::cli::task::args::TaskStatusArg::Draft
|| args.parent_status.is_some()
|| args.leaf_status.is_some()
|| args.child_policy != TaskDecomposeChildPolicyArg::Fail
|| args.runner.is_some()
|| args.model.is_some()
|| args.effort.is_some()
|| args.repo_prompt.is_some()
{
bail!(
"`cueloop task decompose --from-preview` replays saved preview options and cannot be combined with planner/status flags. Do not add --leaf-status, --parent-status, --with-dependencies, or other planner options; the preview already captured them."
);
}
Ok(())
}
fn source_from_args(
resolved: &config::Resolved,
source_args: &[String],
from_file: Option<&std::path::Path>,
) -> Result<task_cmd::TaskDecomposeSourceInput> {
if let Some(path) = from_file {
if !source_args.is_empty() {
bail!(
"`cueloop task decompose --from-file` cannot be combined with positional SOURCE text."
);
}
return task_cmd::read_plan_file_source(resolved, path);
}
Ok(task_cmd::TaskDecomposeSourceInput::Inline(
task_cmd::read_request_from_args_or_stdin(source_args)?,
))
}
fn child_policy(value: TaskDecomposeChildPolicyArg) -> task_cmd::DecompositionChildPolicy {
match value {
TaskDecomposeChildPolicyArg::Fail => task_cmd::DecompositionChildPolicy::Fail,
TaskDecomposeChildPolicyArg::Append => task_cmd::DecompositionChildPolicy::Append,
TaskDecomposeChildPolicyArg::Replace => task_cmd::DecompositionChildPolicy::Replace,
}
}
fn print_text_output(
preview: &task_cmd::DecompositionPreview,
write_result: Option<&task_cmd::TaskDecomposeWriteResult>,
document: &MachineDecomposeDocument,
) {
match &preview.source {
task_cmd::DecompositionSource::Freeform { request } => {
println!("Decompose preview for new request:");
println!(" {}", request);
}
task_cmd::DecompositionSource::ExistingTask { task } => {
println!("Decompose preview for existing task {}:", task.id);
println!(" {}", task.title);
}
task_cmd::DecompositionSource::PlanFile { path, .. } => {
println!("Decompose preview for plan file:");
println!(" {}", path);
}
}
if let Some(attach_target) = &preview.attach_target {
println!("Attach target:");
println!(" {}: {}", attach_target.task.id, attach_target.task.title);
if attach_target.has_existing_children {
println!(
" Existing child tasks detected; policy {:?} will govern write behavior.",
preview.child_policy
);
}
}
println!();
print_node(&preview.plan.root, 0);
println!();
println!(
"Stats: {} node(s), {} leaf node(s).",
preview.plan.total_nodes, preview.plan.leaf_nodes
);
let actionability = preview.plan.actionability();
println!(
"Root/group task: {} ({}, kind: {}).",
actionability.root_group.title,
actionability
.root_group
.planner_key
.as_deref()
.unwrap_or("root"),
actionability.root_group.kind
);
if let Some(first_leaf) = actionability.first_actionable_leaf.as_ref() {
println!(
"First actionable leaf: {} ({}, kind: {}).",
first_leaf.title,
first_leaf.planner_key.as_deref().unwrap_or("leaf"),
first_leaf.kind
);
}
println!(
"Planner options: child policy {:?}, sibling dependencies {}.",
preview.child_policy,
if preview.with_dependencies {
"enabled"
} else {
"disabled"
}
);
if !preview.plan.dependency_edges.is_empty() {
println!("Dependency edges:");
for edge in &preview.plan.dependency_edges {
println!(
" - {} depends on {}",
edge.task_title, edge.depends_on_title
);
}
}
if !preview.plan.warnings.is_empty() {
println!("Decomposition notes:");
for warning in &preview.plan.warnings {
println!(" - {}", warning);
}
}
if let Some(result) = write_result {
println!();
if let Some(root_id) = &result.root_task_id {
println!("Wrote decomposition rooted at {}.", root_id);
} else if let Some(parent_id) = &result.parent_task_id {
println!("Wrote decomposition under existing parent {}.", parent_id);
}
if let Some(root_group_id) = &result.root_group_task_id {
println!("Root/group task: {}.", root_group_id);
}
if let Some(first_leaf_id) = &result.first_actionable_leaf_task_id {
println!("First actionable leaf task: {}.", first_leaf_id);
}
println!("Created {} task(s):", result.created_ids.len());
for id in &result.created_ids {
println!(" - {}", id);
}
if !result.replaced_ids.is_empty() {
println!(
"Replaced {} prior child task(s):",
result.replaced_ids.len()
);
for id in &result.replaced_ids {
println!(" - {}", id);
}
}
if result.parent_annotated
&& let Some(parent_id) = &result.parent_task_id
{
println!(
"Annotated parent task {} with a decomposition note.",
parent_id
);
}
}
println!();
println!("{}", document.continuation.headline);
println!("{}", document.continuation.detail);
if let Some(blocking) = document
.blocking
.as_ref()
.or(document.continuation.blocking.as_ref())
{
println!();
println!(
"Operator state: {}",
format!("{:?}", blocking.status).to_lowercase()
);
println!("{}", blocking.message);
if !blocking.detail.is_empty() {
println!("{}", blocking.detail);
}
}
if !document.continuation.next_steps.is_empty() {
println!();
println!("Next:");
for (index, step) in document.continuation.next_steps.iter().enumerate() {
println!(" {}. {} — {}", index + 1, step.command, step.detail);
}
}
}
fn print_node(node: &task_cmd::PlannedNode, depth: usize) {
let deps = if node.depends_on_keys.is_empty() {
String::new()
} else {
format!(" [depends_on: {}]", node.depends_on_keys.join(", "))
};
println!(
"{}- {} ({}){}",
" ".repeat(depth),
node.title,
node.planner_key,
deps
);
for child in &node.children {
print_node(child, depth + 1);
}
}