use crate::git_utils::get_worker_commits;
use anyhow::{Context, Result};
use cctakt::{Config, Plan, TaskAction, TaskResult, TaskStatus, WorktreeManager};
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Command, Stdio};
pub fn run_plan(plan_path: PathBuf) -> Result<()> {
println!("Loading plan from: {}", plan_path.display());
let plan_content = fs::read_to_string(&plan_path)
.with_context(|| format!("Failed to read plan file: {}", plan_path.display()))?;
let mut plan: Plan =
serde_json::from_str(&plan_content).with_context(|| "Failed to parse plan JSON")?;
println!(
"Plan: {}",
plan.description.as_deref().unwrap_or("(no description)")
);
println!("Tasks: {}", plan.tasks.len());
println!();
let config = Config::load().unwrap_or_default();
let worktree_manager =
WorktreeManager::from_current_dir().context("Failed to initialize worktree manager")?;
for task in &mut plan.tasks {
if task.status != TaskStatus::Pending {
println!("[{}] Skipping (status: {:?})", task.id, task.status);
continue;
}
let TaskAction::CreateWorker {
branch,
task_description,
base_branch: _,
} = &task.action
else {
println!("[{}] Skipping (not a create_worker task)", task.id);
continue;
};
println!("========================================");
println!("[{}] Starting worker", task.id);
println!("Branch: {branch}");
println!("Task: {}", task_description.lines().next().unwrap_or(""));
println!("========================================");
let worktree_path = match worktree_manager.create(branch, &config.worktree_dir) {
Ok(path) => {
println!("Created worktree: {}", path.display());
path
}
Err(e) => {
println!("Failed to create worktree: {e}");
task.status = TaskStatus::Failed;
task.error = Some(format!("Failed to create worktree: {e}"));
continue;
}
};
task.status = TaskStatus::Running;
let mut cmd = Command::new("claude");
cmd.arg("-p")
.arg(task_description)
.arg("--output-format")
.arg("stream-json")
.arg("--verbose")
.arg("--dangerously-skip-permissions");
cmd.current_dir(&worktree_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
println!("\n--- Worker output ---\n");
let mut child = cmd.spawn().context("Failed to spawn claude")?;
if let Some(stdout) = child.stdout.take() {
let reader = BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {
let event_type = json.get("type").and_then(|v| v.as_str()).unwrap_or("");
match event_type {
"system" => {
let subtype =
json.get("subtype").and_then(|v| v.as_str()).unwrap_or("");
println!("[SYS] {subtype}");
}
"assistant" => {
if let Some(content) = json
.get("message")
.and_then(|m| m.get("content"))
.and_then(|c| c.as_array())
{
for block in content {
if block.get("type").and_then(|t| t.as_str()) == Some("text") {
if let Some(text) =
block.get("text").and_then(|t| t.as_str())
{
let preview: String = text.chars().take(100).collect();
if !preview.trim().is_empty() {
println!(
"[AI] {}...",
preview.replace('\n', " ")
);
}
}
}
}
}
}
"result" => {
let subtype =
json.get("subtype").and_then(|v| v.as_str()).unwrap_or("");
println!("[RESULT] {subtype}");
}
_ => {}
}
}
}
}
let status = child.wait()?;
println!("\n--- Worker finished (exit: {status}) ---\n");
let commits = get_worker_commits(&worktree_path);
println!("Commits: {}", commits.len());
for commit in &commits {
println!(" - {commit}");
}
if status.success() {
task.status = TaskStatus::Completed;
task.result = Some(TaskResult {
commits,
pr_number: None,
pr_url: None,
});
} else {
task.status = TaskStatus::Failed;
task.error = Some(format!("Process exited with: {status}"));
}
println!();
}
let updated_plan = serde_json::to_string_pretty(&plan)?;
fs::write(&plan_path, updated_plan)?;
println!("Plan saved to: {}", plan_path.display());
Ok(())
}