Skip to main content

chant/operations/
reset.rs

1//! Spec reset operation.
2//!
3//! Canonical implementation for resetting specs to pending status.
4
5use anyhow::Result;
6use std::path::Path;
7
8use crate::spec::{Spec, SpecStatus};
9
10/// Options for spec reset
11#[derive(Debug, Clone, Default)]
12pub struct ResetOptions {
13    /// Whether to re-execute the spec after reset
14    pub re_execute: bool,
15    /// Optional prompt template for re-execution
16    pub prompt: Option<String>,
17    /// Optional branch to use for re-execution
18    pub branch: Option<String>,
19}
20
21/// Reset a spec to pending status.
22///
23/// This is the canonical reset logic used by both CLI and MCP.
24/// Only failed or in_progress specs can be reset.
25pub fn reset_spec(spec: &mut Spec, spec_path: &Path, _options: ResetOptions) -> Result<()> {
26    // Check if spec is in failed or in_progress state
27    if spec.frontmatter.status != SpecStatus::Failed
28        && spec.frontmatter.status != SpecStatus::InProgress
29    {
30        anyhow::bail!(
31            "Spec '{}' is not in failed or in_progress state (current: {:?}). \
32             Only failed or in_progress specs can be reset.",
33            spec.id,
34            spec.frontmatter.status
35        );
36    }
37
38    let spec_id = &spec.id;
39
40    // Clean up resources: lock file, PID file, worktree, branch
41    // Use best-effort cleanup - don't fail if resources don't exist
42    // These may legitimately be missing if the spec was never started or already cleaned up
43
44    // Remove lock file
45    if let Err(e) = crate::lock::remove_lock(spec_id) {
46        eprintln!(
47            "Warning: Failed to remove lock file for spec {}: {}",
48            spec_id, e
49        );
50    }
51
52    // Remove PID file
53    if let Err(e) = crate::pid::remove_pid_file(spec_id) {
54        eprintln!(
55            "Warning: Failed to remove PID file for spec {}: {}",
56            spec_id, e
57        );
58    }
59
60    // Remove process files
61    if let Err(e) = crate::pid::remove_process_files(spec_id) {
62        eprintln!(
63            "Warning: Failed to remove process files for spec {}: {}",
64            spec_id, e
65        );
66    }
67
68    // Remove worktree if it exists
69    if let Ok(config) = crate::config::Config::load() {
70        let project_name = Some(config.project.name.as_str());
71        if let Some(worktree_path) = crate::worktree::get_active_worktree(spec_id, project_name) {
72            if let Err(e) = crate::worktree::remove_worktree(&worktree_path) {
73                eprintln!(
74                    "Warning: Failed to remove worktree for spec {}: {}",
75                    spec_id, e
76                );
77            }
78        }
79    }
80
81    // Remove branch if it exists
82    if let Ok(config) = crate::config::Config::load() {
83        let branch_prefix = &config.defaults.branch_prefix;
84        let branch = format!("{}{}", branch_prefix, spec_id);
85        if let Err(e) = std::process::Command::new("git")
86            .args(["branch", "-D", &branch])
87            .output()
88        {
89            eprintln!("Warning: Failed to delete git branch {}: {}", branch, e);
90        }
91    }
92
93    // Reset to pending using state machine
94    spec.set_status(SpecStatus::Pending)
95        .map_err(|e| anyhow::anyhow!("Failed to transition spec to Pending: {}", e))?;
96
97    // Save the spec
98    spec.save(spec_path)?;
99
100    Ok(())
101}