Skip to main content

cersei_tools/
worktree.rs

1//! Worktree tools: create/exit isolated git worktrees for parallel work.
2
3use super::*;
4use serde::Deserialize;
5use std::process::Stdio;
6
7pub struct EnterWorktreeTool;
8
9#[async_trait]
10impl Tool for EnterWorktreeTool {
11    fn name(&self) -> &str {
12        "EnterWorktree"
13    }
14    fn description(&self) -> &str {
15        "Create an isolated git worktree for the agent to work in without affecting the main branch."
16    }
17    fn permission_level(&self) -> PermissionLevel {
18        PermissionLevel::Write
19    }
20    fn category(&self) -> ToolCategory {
21        ToolCategory::FileSystem
22    }
23
24    fn input_schema(&self) -> Value {
25        serde_json::json!({
26            "type": "object",
27            "properties": {
28                "branch": { "type": "string", "description": "Branch name for the worktree" },
29                "path": { "type": "string", "description": "Optional path for the worktree (default: auto-generated)" }
30            },
31            "required": ["branch"]
32        })
33    }
34
35    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
36        #[derive(Deserialize)]
37        struct Input {
38            branch: String,
39            path: Option<String>,
40        }
41
42        let input: Input = match serde_json::from_value(input) {
43            Ok(i) => i,
44            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
45        };
46
47        let worktree_path = input.path.unwrap_or_else(|| {
48            let tmp = std::env::temp_dir().join(format!("cersei-wt-{}", &input.branch));
49            tmp.display().to_string()
50        });
51
52        let output = tokio::process::Command::new("git")
53            .args(["worktree", "add", "-b", &input.branch, &worktree_path])
54            .current_dir(&ctx.working_dir)
55            .stdout(Stdio::piped())
56            .stderr(Stdio::piped())
57            .output()
58            .await;
59
60        match output {
61            Ok(o) if o.status.success() => ToolResult::success(format!(
62                "Worktree created at: {}\nBranch: {}",
63                worktree_path, input.branch
64            )),
65            Ok(o) => {
66                let stderr = String::from_utf8_lossy(&o.stderr);
67                ToolResult::error(format!("git worktree failed: {}", stderr))
68            }
69            Err(e) => ToolResult::error(format!("Failed to run git: {}", e)),
70        }
71    }
72}
73
74pub struct ExitWorktreeTool;
75
76#[async_trait]
77impl Tool for ExitWorktreeTool {
78    fn name(&self) -> &str {
79        "ExitWorktree"
80    }
81    fn description(&self) -> &str {
82        "Remove a git worktree."
83    }
84    fn permission_level(&self) -> PermissionLevel {
85        PermissionLevel::Write
86    }
87    fn category(&self) -> ToolCategory {
88        ToolCategory::FileSystem
89    }
90
91    fn input_schema(&self) -> Value {
92        serde_json::json!({
93            "type": "object",
94            "properties": {
95                "path": { "type": "string", "description": "Path of the worktree to remove" }
96            },
97            "required": ["path"]
98        })
99    }
100
101    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
102        #[derive(Deserialize)]
103        struct Input {
104            path: String,
105        }
106
107        let input: Input = match serde_json::from_value(input) {
108            Ok(i) => i,
109            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
110        };
111
112        let output = tokio::process::Command::new("git")
113            .args(["worktree", "remove", "--force", &input.path])
114            .current_dir(&ctx.working_dir)
115            .stdout(Stdio::piped())
116            .stderr(Stdio::piped())
117            .output()
118            .await;
119
120        match output {
121            Ok(o) if o.status.success() => {
122                ToolResult::success(format!("Worktree removed: {}", input.path))
123            }
124            Ok(o) => {
125                let stderr = String::from_utf8_lossy(&o.stderr);
126                ToolResult::error(format!("git worktree remove failed: {}", stderr))
127            }
128            Err(e) => ToolResult::error(format!("Failed to run git: {}", e)),
129        }
130    }
131}