use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanDocument {
pub id: Uuid,
pub session_id: Uuid,
pub title: String,
pub description: String,
pub tasks: Vec<PlanTask>,
pub context: String,
pub risks: Vec<String>,
pub test_strategy: String,
pub technical_stack: Vec<String>,
pub status: PlanStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub approved_at: Option<DateTime<Utc>>,
}
impl PlanDocument {
pub fn new(session_id: Uuid, title: String, description: String) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
title,
description,
tasks: Vec::new(),
context: String::new(),
risks: Vec::new(),
test_strategy: String::new(),
technical_stack: Vec::new(),
status: PlanStatus::Draft,
created_at: Utc::now(),
updated_at: Utc::now(),
approved_at: None,
}
}
pub fn add_task(&mut self, task: PlanTask) {
self.tasks.push(task);
self.updated_at = Utc::now();
}
pub fn tasks_in_order(&self) -> Option<Vec<&PlanTask>> {
use std::collections::{HashMap, VecDeque};
let mut in_degree: HashMap<Uuid, usize> = HashMap::new();
let mut dependents: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
for task in &self.tasks {
in_degree.insert(task.id, task.dependencies.len());
for &dep_id in &task.dependencies {
dependents.entry(dep_id).or_default().push(task.id);
}
}
let mut queue: VecDeque<Uuid> = VecDeque::new();
for task in &self.tasks {
if task.dependencies.is_empty() {
queue.push_back(task.id);
}
}
let mut sorted_ids = Vec::new();
while let Some(task_id) = queue.pop_front() {
sorted_ids.push(task_id);
if let Some(deps) = dependents.get(&task_id) {
for &dependent_id in deps {
if let Some(degree) = in_degree.get_mut(&dependent_id) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dependent_id);
}
}
}
}
}
if sorted_ids.len() != self.tasks.len() {
return None; }
let task_map: HashMap<Uuid, &PlanTask> = self.tasks.iter().map(|t| (t.id, t)).collect();
Some(
sorted_ids
.iter()
.filter_map(|id| task_map.get(id).copied())
.collect(),
)
}
pub fn get_task(&self, task_id: &Uuid) -> Option<&PlanTask> {
self.tasks.iter().find(|t| t.id == *task_id)
}
pub fn get_task_mut(&mut self, task_id: &Uuid) -> Option<&mut PlanTask> {
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|t| t.id == *task_id)
}
pub fn count_by_status(&self, status: TaskStatus) -> usize {
self.tasks.iter().filter(|t| t.status == status).count()
}
pub fn progress_percentage(&self) -> f32 {
if self.tasks.is_empty() {
return 0.0;
}
let completed = self.count_by_status(TaskStatus::Completed);
(completed as f32 / self.tasks.len() as f32) * 100.0
}
pub fn is_complete(&self) -> bool {
!self.tasks.is_empty()
&& self
.tasks
.iter()
.all(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
}
pub fn approve(&mut self) {
self.status = PlanStatus::Approved;
self.approved_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn reject(&mut self) {
self.status = PlanStatus::Rejected;
self.updated_at = Utc::now();
}
pub fn start_execution(&mut self) {
self.status = PlanStatus::InProgress;
self.updated_at = Utc::now();
}
pub fn complete(&mut self) {
self.status = PlanStatus::Completed;
self.updated_at = Utc::now();
}
pub fn validate_dependencies(&self) -> Result<(), String> {
let task_ids: std::collections::HashSet<Uuid> = self.tasks.iter().map(|t| t.id).collect();
for task in &self.tasks {
for &dep_id in &task.dependencies {
if !task_ids.contains(&dep_id) {
return Err(format!(
"❌ Invalid Dependency\n\n\
Task '{}' (#{}) depends on a task that doesn't exist.\n\n\
💡 Fix: Remove this dependency or ensure the referenced task is added first.",
task.title, task.order
));
}
}
}
let ordered = self.tasks_in_order();
if ordered.is_none() {
let unprocessed: Vec<&str> = self
.tasks
.iter()
.filter(|task| !task.dependencies.is_empty())
.map(|task| task.title.as_str())
.collect();
return Err(format!(
"❌ Circular Dependency Detected\n\n\
Tasks with dependencies: {}\n\n\
💡 Fix: Review the dependency chain and remove circular references.\n\
Example: If Task A depends on B, B depends on C, and C depends on A,\n\
you need to break one of these dependency links.",
unprocessed.join(", ")
));
}
Ok(())
}
pub fn next_executable_task(&self) -> Option<&PlanTask> {
let completed_ids: std::collections::HashSet<Uuid> = self
.tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
.map(|t| t.id)
.collect();
self.tasks.iter().find(|task| {
matches!(task.status, TaskStatus::Pending)
&& task
.dependencies
.iter()
.all(|dep| completed_ids.contains(dep))
})
}
pub fn next_executable_task_mut(&mut self) -> Option<&mut PlanTask> {
let completed_ids: std::collections::HashSet<Uuid> = self
.tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
.map(|t| t.id)
.collect();
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|task| {
matches!(task.status, TaskStatus::Pending)
&& task
.dependencies
.iter()
.all(|dep| completed_ids.contains(dep))
})
}
pub fn get_task_by_order(&self, order: usize) -> Option<&PlanTask> {
self.tasks.iter().find(|t| t.order == order)
}
pub fn get_task_by_order_mut(&mut self, order: usize) -> Option<&mut PlanTask> {
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|t| t.order == order)
}
pub fn dependencies_satisfied(&self, task: &PlanTask) -> bool {
task.dependencies.iter().all(|dep_id| {
self.get_task(dep_id)
.map(|dep| matches!(dep.status, TaskStatus::Completed | TaskStatus::Skipped))
.unwrap_or(false)
})
}
pub fn execution_summary(&self) -> ExecutionSummary {
let mut summary = ExecutionSummary::default();
for task in &self.tasks {
summary.total_tasks += 1;
match task.status {
TaskStatus::Completed => summary.completed += 1,
TaskStatus::Failed => summary.failed += 1,
TaskStatus::InProgress => summary.in_progress += 1,
TaskStatus::Pending => summary.pending += 1,
TaskStatus::Skipped => summary.skipped += 1,
TaskStatus::Blocked(_) => summary.blocked += 1,
}
summary.total_retries += task.retry_count as usize;
summary.total_tool_calls += task
.execution_history
.iter()
.map(|e| e.tools_called.len())
.sum::<usize>();
}
summary.success_rate = if summary.completed + summary.failed > 0 {
(summary.completed as f32 / (summary.completed + summary.failed) as f32) * 100.0
} else {
0.0
};
summary
}
pub fn ready_tasks(&self) -> Vec<&PlanTask> {
self.tasks
.iter()
.filter(|task| {
matches!(task.status, TaskStatus::Pending) && self.dependencies_satisfied(task)
})
.collect()
}
pub fn retriable_tasks(&self) -> Vec<&PlanTask> {
self.tasks.iter().filter(|task| task.can_retry()).collect()
}
pub fn get_validation_warnings(&self) -> Vec<String> {
let mut warnings = Vec::new();
for task in &self.tasks {
if task.complexity >= 5 {
warnings.push(format!(
"⚠️ Task '{}' has maximum complexity ({}★) - consider breaking it down",
task.title, task.complexity
));
}
if task.description.len() < 50 {
warnings.push(format!(
"💡 Task '{}' has a brief description ({} chars) - add more detail",
task.title,
task.description.len()
));
}
if task.acceptance_criteria.is_empty() {
warnings.push(format!(
"💡 Task '{}' has no acceptance criteria - define success criteria",
task.title
));
}
}
if self.tasks.len() > 20 {
warnings.push(format!(
"⚠️ Plan has {} tasks (>20) - consider splitting into smaller plans",
self.tasks.len()
));
}
if self.context.is_empty() {
warnings
.push("💡 Plan has no context - add environment info or constraints".to_string());
}
if self.risks.is_empty() {
warnings
.push("💡 Plan has no identified risks - document potential issues".to_string());
}
if self.test_strategy.is_empty() {
warnings
.push("💡 Plan has no test strategy - define how to verify success".to_string());
}
warnings
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExecutionSummary {
pub total_tasks: usize,
pub completed: usize,
pub failed: usize,
pub in_progress: usize,
pub pending: usize,
pub skipped: usize,
pub blocked: usize,
pub total_retries: usize,
pub total_tool_calls: usize,
pub success_rate: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PlanStatus {
Draft,
PendingApproval,
Approved,
Rejected,
InProgress,
Completed,
Cancelled,
}
impl std::fmt::Display for PlanStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanStatus::Draft => write!(f, "Draft"),
PlanStatus::PendingApproval => write!(f, "Pending Approval"),
PlanStatus::Approved => write!(f, "Approved"),
PlanStatus::Rejected => write!(f, "Rejected"),
PlanStatus::InProgress => write!(f, "In Progress"),
PlanStatus::Completed => write!(f, "Completed"),
PlanStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanTask {
pub id: Uuid,
pub order: usize,
pub title: String,
pub description: String,
pub task_type: TaskType,
pub dependencies: Vec<Uuid>,
pub complexity: u8,
pub acceptance_criteria: Vec<String>,
pub status: TaskStatus,
pub notes: Option<String>,
pub completed_at: Option<DateTime<Utc>>,
#[serde(default)]
pub execution_history: Vec<TaskExecution>,
#[serde(default)]
pub retry_count: u8,
#[serde(default = "default_max_retries")]
pub max_retries: u8,
#[serde(default)]
pub artifacts: Vec<String>,
#[serde(default)]
pub reflection: Option<String>,
}
fn default_max_retries() -> u8 {
3
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskExecution {
pub started_at: DateTime<Utc>,
pub ended_at: Option<DateTime<Utc>>,
pub tools_called: Vec<ToolCall>,
pub output: Option<String>,
pub error: Option<String>,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub tool_name: String,
pub input: serde_json::Value,
pub output: Option<String>,
pub success: bool,
pub timestamp: DateTime<Utc>,
}
impl PlanTask {
pub fn new(order: usize, title: String, description: String, task_type: TaskType) -> Self {
Self {
id: Uuid::new_v4(),
order,
title,
description,
task_type,
dependencies: Vec::new(),
complexity: 3, acceptance_criteria: Vec::new(),
status: TaskStatus::Pending,
notes: None,
completed_at: None,
execution_history: Vec::new(),
retry_count: 0,
max_retries: 3,
artifacts: Vec::new(),
reflection: None,
}
}
pub fn start(&mut self) {
self.status = TaskStatus::InProgress;
}
pub fn start_execution(&mut self) -> &mut TaskExecution {
self.status = TaskStatus::InProgress;
let execution = TaskExecution {
started_at: Utc::now(),
ended_at: None,
tools_called: Vec::new(),
output: None,
error: None,
success: false,
};
self.execution_history.push(execution);
self.execution_history.last_mut().expect("just pushed")
}
pub fn record_tool_call(&mut self, tool_call: ToolCall) {
if let Some(execution) = self.execution_history.last_mut() {
execution.tools_called.push(tool_call);
}
}
pub fn complete_execution(&mut self, output: String, success: bool) {
if let Some(execution) = self.execution_history.last_mut() {
execution.ended_at = Some(Utc::now());
execution.output = Some(output.clone());
execution.success = success;
}
if success {
self.status = TaskStatus::Completed;
self.notes = Some(output);
self.completed_at = Some(Utc::now());
} else {
self.retry_count += 1;
if self.retry_count >= self.max_retries {
self.status = TaskStatus::Failed;
} else {
self.status = TaskStatus::Pending; }
}
}
pub fn fail_execution(&mut self, error: String) {
if let Some(execution) = self.execution_history.last_mut() {
execution.ended_at = Some(Utc::now());
execution.error = Some(error.clone());
execution.success = false;
}
self.retry_count += 1;
if self.retry_count >= self.max_retries {
self.status = TaskStatus::Failed;
self.notes = Some(format!(
"Failed after {} attempts: {}",
self.retry_count, error
));
} else {
self.status = TaskStatus::Pending;
}
}
pub fn add_reflection(&mut self, reflection: String) {
self.reflection = Some(reflection);
}
pub fn add_artifact(&mut self, artifact: String) {
self.artifacts.push(artifact);
}
pub fn can_retry(&self) -> bool {
self.retry_count < self.max_retries
&& matches!(self.status, TaskStatus::Pending | TaskStatus::Failed)
}
pub fn last_execution(&self) -> Option<&TaskExecution> {
self.execution_history.last()
}
pub fn complete(&mut self, notes: Option<String>) {
self.status = TaskStatus::Completed;
self.notes = notes;
self.completed_at = Some(Utc::now());
}
pub fn fail(&mut self, reason: String) {
self.status = TaskStatus::Failed;
self.notes = Some(reason);
}
pub fn block(&mut self, reason: String) {
self.status = TaskStatus::Blocked(reason);
}
pub fn skip(&mut self, reason: Option<String>) {
self.status = TaskStatus::Skipped;
if let Some(r) = reason {
self.notes = Some(r);
}
}
pub fn complexity_stars(&self) -> String {
let filled = self.complexity.min(5);
let empty = 5 - filled;
"★".repeat(filled as usize) + &"☆".repeat(empty as usize)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskType {
Research,
Edit,
Create,
Delete,
Test,
Refactor,
Documentation,
Configuration,
Build,
Other(String),
}
impl std::fmt::Display for TaskType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskType::Research => write!(f, "Research"),
TaskType::Edit => write!(f, "Edit"),
TaskType::Create => write!(f, "Create"),
TaskType::Delete => write!(f, "Delete"),
TaskType::Test => write!(f, "Test"),
TaskType::Refactor => write!(f, "Refactor"),
TaskType::Documentation => write!(f, "Documentation"),
TaskType::Configuration => write!(f, "Configuration"),
TaskType::Build => write!(f, "Build"),
TaskType::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskStatus {
Pending,
InProgress,
Completed,
Skipped,
Failed,
Blocked(String),
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskStatus::Pending => write!(f, "Pending"),
TaskStatus::InProgress => write!(f, "In Progress"),
TaskStatus::Completed => write!(f, "Completed"),
TaskStatus::Skipped => write!(f, "Skipped"),
TaskStatus::Failed => write!(f, "Failed"),
TaskStatus::Blocked(reason) => write!(f, "Blocked: {}", reason),
}
}
}
impl TaskStatus {
pub fn icon(&self) -> &str {
match self {
TaskStatus::Pending => "⏸️",
TaskStatus::InProgress => "▶️",
TaskStatus::Completed => "✅",
TaskStatus::Skipped => "⏭️",
TaskStatus::Failed => "❌",
TaskStatus::Blocked(_) => "🚫",
}
}
}
#[cfg(test)]
#[path = "plan_tests.rs"]
mod plan_tests;