ralph-agent-loop 0.4.0

A Rust CLI for managing AI agent loops with a structured JSON task queue
Documentation
//! Queue prune subcommand.

use anyhow::Result;
use clap::Args;

use crate::config::Resolved;
use crate::queue;

use super::StatusArg;

/// Arguments for `ralph queue prune`.
#[derive(Args)]
#[command(
    after_long_help = "Prune removes old tasks from .ralph/done.jsonc while preserving recent history.\n\nSafety:\n  --keep-last always protects the N most recently completed tasks (by completed_at).\n  If no filters are provided, all tasks are pruned except those protected by --keep-last.\n  Missing or invalid completed_at timestamps are treated as oldest for keep-last ordering\n  but do NOT match the age filter (safety-first).\n\nExamples:\n  ralph queue prune --dry-run --age 30 --status rejected\n  ralph queue prune --keep-last 100\n  ralph queue prune --age 90\n  ralph queue prune --age 30 --status done --keep-last 50"
)]
pub struct QueuePruneArgs {
    /// Only prune tasks completed at least N days ago.
    #[arg(long)]
    pub age: Option<u32>,

    /// Filter by task status (repeatable).
    #[arg(long, value_enum)]
    pub status: Vec<StatusArg>,

    /// Keep the N most recently completed tasks regardless of filters.
    #[arg(long)]
    pub keep_last: Option<u32>,

    /// Show what would be pruned without writing to disk.
    #[arg(long)]
    pub dry_run: bool,
}

pub(crate) fn handle(resolved: &Resolved, force: bool, args: QueuePruneArgs) -> Result<()> {
    let _queue_lock = queue::acquire_queue_lock(&resolved.repo_root, "queue prune", force)?;

    // Create undo snapshot before mutation (only if not dry-run)
    if !args.dry_run {
        crate::undo::create_undo_snapshot(resolved, "queue prune")?;
    }

    let report: queue::PruneReport = queue::prune_done_tasks(
        &resolved.done_path,
        queue::PruneOptions {
            age_days: args.age,
            statuses: args.status.into_iter().map(|s| s.into()).collect(),
            keep_last: args.keep_last,
            dry_run: args.dry_run,
        },
    )?;
    if args.dry_run {
        log::info!("Dry run: would prune {} task(s).", report.pruned_ids.len());
        if !report.pruned_ids.is_empty() {
            log::info!("Pruned IDs: {}", report.pruned_ids.join(", "));
        }
        if !report.kept_ids.is_empty() {
            log::info!("Kept IDs: {}", report.kept_ids.join(", "));
        }
    } else {
        if report.pruned_ids.is_empty() {
            log::info!("No tasks pruned.");
        } else {
            log::info!("Pruned {} task(s).", report.pruned_ids.len());
        }
        if !report.kept_ids.is_empty() {
            log::debug!("Kept {} task(s).", report.kept_ids.len());
        }
    }
    Ok(())
}