Skip to main content

ralph/commands/task/update/
mod.rs

1//! Task updating orchestration for runner-driven task refreshes.
2//!
3//! Responsibilities:
4//! - Expose single-task and update-all entrypoints for `ralph task update`.
5//! - Coordinate dry-run previews, queue locking, backup creation, runner execution, and result reporting.
6//! - Keep the root module as a small facade over focused queue, runner, and reporting helpers.
7//!
8//! Not handled here:
9//! - Prompt rendering and runner invocation details.
10//! - Queue backup/restore internals or validation helper implementations.
11//! - Field-diff reporting logic or unit-test fixtures.
12//!
13//! Invariants/assumptions:
14//! - Dry-run mode must remain side-effect free with respect to queue/done files.
15//! - Non-dry-run updates create a backup before invoking the runner.
16//! - Queue locks are optional only for the internal no-lock/update-all flows.
17
18mod preview;
19mod reporting;
20mod runner;
21mod state;
22#[cfg(test)]
23mod tests;
24
25use super::TaskUpdateSettings;
26use crate::{config, queue};
27use anyhow::{Context, Result, bail};
28
29pub fn update_task(
30    resolved: &config::Resolved,
31    task_id: &str,
32    settings: &TaskUpdateSettings,
33) -> Result<()> {
34    update_task_impl(resolved, task_id, settings, true)
35}
36
37pub fn update_task_without_lock(
38    resolved: &config::Resolved,
39    task_id: &str,
40    settings: &TaskUpdateSettings,
41) -> Result<()> {
42    update_task_impl(resolved, task_id, settings, false)
43}
44
45pub fn update_all_tasks(resolved: &config::Resolved, settings: &TaskUpdateSettings) -> Result<()> {
46    let _queue_lock =
47        queue::acquire_queue_lock(&resolved.repo_root, "task update", settings.force)?;
48
49    let queue_file = queue::load_queue(&resolved.queue_path)
50        .with_context(|| format!("read queue {}", resolved.queue_path.display()))?;
51
52    if queue_file.tasks.is_empty() {
53        bail!("No tasks in queue to update.");
54    }
55
56    let task_ids: Vec<String> = queue_file
57        .tasks
58        .iter()
59        .map(|task| task.id.clone())
60        .collect();
61    for task_id in task_ids {
62        update_task_impl(resolved, &task_id, settings, false)?;
63    }
64
65    Ok(())
66}
67
68fn update_task_impl(
69    resolved: &config::Resolved,
70    task_id: &str,
71    settings: &TaskUpdateSettings,
72    acquire_lock: bool,
73) -> Result<()> {
74    if settings.dry_run {
75        return preview::preview_task_update(resolved, task_id, settings);
76    }
77
78    let _queue_lock = if acquire_lock {
79        Some(queue::acquire_queue_lock(
80            &resolved.repo_root,
81            "task update",
82            settings.force,
83        )?)
84    } else {
85        None
86    };
87
88    let backup_path = state::backup_queue_for_update(resolved)?;
89    let prepared = state::prepare_task_update(resolved, task_id)?;
90    let prompt = runner::build_task_update_prompt(resolved, prepared.task_id.as_str(), settings)?;
91
92    runner::run_task_updater(resolved, settings, &prompt)?;
93
94    let after = state::load_validate_and_save_queue_after_update(
95        resolved,
96        &backup_path,
97        prepared.max_depth,
98    )?;
99    let done_after = state::load_done_queue(resolved)?;
100
101    if let Some(after_task) = state::find_task(&after, prepared.task_id.as_str()) {
102        return reporting::log_task_update_changes(
103            &prepared.before_json,
104            prepared.task_id.as_str(),
105            "updated",
106            Some(after_task),
107        );
108    }
109
110    reporting::log_task_update_changes(
111        &prepared.before_json,
112        prepared.task_id.as_str(),
113        "moved to done.jsonc",
114        state::find_task(&done_after, prepared.task_id.as_str()),
115    )
116}