use anyhow::{Result, bail};
use crate::agent;
use crate::cli::task::args::{TaskEditArgs, TaskFieldArgs, TaskUpdateArgs};
use crate::commands::task as task_cmd;
use crate::config;
use crate::queue;
use crate::queue::operations::{
batch_apply_edit, batch_set_field, print_batch_results, resolve_task_ids,
};
use crate::timeutil;
pub fn handle_field(args: &TaskFieldArgs, force: bool, resolved: &config::Resolved) -> Result<()> {
let queue_file = queue::load_queue(&resolved.queue_path)?;
let task_ids = resolve_task_ids(&queue_file, &args.task_ids, &args.tag_filter)?;
if task_ids.is_empty() {
bail!("No tasks specified. Provide task IDs or use --tag-filter.");
}
if args.dry_run {
println!("Dry run - would update {} tasks:", task_ids.len());
for task_id in &task_ids {
let preview =
queue::operations::preview_set_field(&queue_file, task_id, &args.key, &args.value)?;
println!(" {}:", preview.task_id);
println!(" Field: {}", preview.key);
println!(
" Old: {}",
preview.old_value.as_deref().unwrap_or("(not set)")
);
println!(" New: {}", preview.new_value);
}
println!("\nDry run complete. No changes made.");
return Ok(());
}
queue::with_locked_queue_mutation(
resolved,
"task field",
format!(
"task field {}={} [{}]",
args.key,
args.value,
task_ids.join(", ")
),
force,
|| {
let mut queue_file = queue::load_queue(&resolved.queue_path)?;
let now = timeutil::now_utc_rfc3339()?;
let result = batch_set_field(
&mut queue_file,
&task_ids,
&args.key,
&args.value,
&now,
false,
)?;
queue::save_queue(&resolved.queue_path, &queue_file)?;
print_batch_results(
&result,
&format!("Field set '{}' = '{}'", args.key, args.value),
false,
);
Ok(())
},
)
}
pub fn handle_edit(args: &TaskEditArgs, force: bool, resolved: &config::Resolved) -> Result<()> {
let queue_file = queue::load_queue(&resolved.queue_path)?;
let done_file = queue::load_queue_or_default(&resolved.done_path)?;
let done_ref = queue::optional_done_queue(&done_file, &resolved.done_path);
let now = timeutil::now_utc_rfc3339()?;
let max_depth = resolved.config.queue.max_dependency_depth.unwrap_or(10);
let task_ids = resolve_task_ids(&queue_file, &args.task_ids, &args.tag_filter)?;
if task_ids.is_empty() {
bail!("No tasks specified. Provide task IDs or use --tag-filter.");
}
if args.dry_run {
println!("Dry run - would update {} tasks:", task_ids.len());
for task_id in &task_ids {
let preview = queue::preview_task_edit(
&queue_file,
done_ref,
task_id,
args.field.into(),
&args.value,
&now,
&resolved.id_prefix,
resolved.id_width,
max_depth,
)?;
println!(" {}:", preview.task_id);
println!(" Field: {}", preview.field);
println!(" Old: {}", preview.old_value);
println!(" New: {}", preview.new_value);
if !preview.warnings.is_empty() {
println!(" Warnings:");
for warning in &preview.warnings {
println!(" - [{}] {}", warning.task_id, warning.message);
}
}
}
println!("\nDry run complete. No changes made.");
return Ok(());
}
queue::with_locked_queue_mutation(
resolved,
"task edit",
format!(
"task edit {} [{}]",
args.field.as_str(),
task_ids.join(", ")
),
force,
|| {
let mut queue_file = queue::load_queue(&resolved.queue_path)?;
let mut done_file = queue::load_queue_or_default(&resolved.done_path)?;
let result = batch_apply_edit(
&mut queue_file,
queue::optional_done_queue(&done_file, &resolved.done_path),
&task_ids,
args.field.into(),
&args.value,
&now,
&resolved.id_prefix,
resolved.id_width,
max_depth,
false,
)?;
let archived_task_ids = auto_archive_if_configured(
resolved,
args.no_auto_archive,
&mut queue_file,
&mut done_file,
&now,
);
queue::save_queue(&resolved.queue_path, &queue_file)?;
if !archived_task_ids.is_empty() {
queue::save_queue(&resolved.done_path, &done_file)?;
}
print_batch_results(
&result,
&format!("Edit field '{}'", args.field.as_str()),
false,
);
print_auto_archive_results(&archived_task_ids);
Ok(())
},
)
}
fn auto_archive_if_configured(
resolved: &config::Resolved,
disabled: bool,
queue_file: &mut crate::contracts::QueueFile,
done_file: &mut crate::contracts::QueueFile,
now: &str,
) -> Vec<String> {
if disabled {
return Vec::new();
}
let Some(days) = resolved.config.queue.auto_archive_terminal_after_days else {
return Vec::new();
};
match queue::maybe_archive_terminal_tasks_in_memory(queue_file, done_file, now, Some(days)) {
Ok(report) => report.moved_ids,
Err(err) => {
log::warn!("Auto-archive sweep failed: {}", err);
Vec::new()
}
}
}
fn print_auto_archive_results(archived_task_ids: &[String]) {
if archived_task_ids.is_empty() {
return;
}
println!(
"Auto-archived {} terminal task(s):",
archived_task_ids.len()
);
for task_id in archived_task_ids {
println!(" - {}", task_id);
}
}
pub fn handle_update(
args: &TaskUpdateArgs,
resolved: &config::Resolved,
force: bool,
) -> Result<()> {
let valid_fields = ["scope", "evidence", "plan", "notes", "tags", "depends_on"];
let fields_to_update = if args.fields.trim().is_empty() || args.fields.trim() == "all" {
"scope,evidence,plan,notes,tags,depends_on".to_string()
} else {
for field in args.fields.split(',') {
if !valid_fields.contains(&field.trim()) {
bail!(
"Invalid field '{}'. Valid fields: {}",
field,
valid_fields.join(", ")
);
}
}
args.fields.clone()
};
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 update_settings = task_cmd::TaskUpdateSettings {
fields: fields_to_update,
runner_override: overrides.runner,
model_override: overrides.model,
reasoning_effort_override: overrides.reasoning_effort,
runner_cli_overrides: overrides.runner_cli,
force,
repoprompt_tool_injection: agent::resolve_rp_required(args.repo_prompt, resolved),
dry_run: args.dry_run,
};
match args.task_id.as_deref() {
Some(task_id) => task_cmd::update_task(resolved, task_id, &update_settings),
None => task_cmd::update_all_tasks(resolved, &update_settings),
}
}