use bamboo_domain::task::{TaskBlocker, TaskEvidence, TaskPhase, TaskPriority, TaskTransition};
use bamboo_domain::TaskItemStatus;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
mod auto_status;
mod conversion;
mod prompt;
mod tracking;
#[derive(Debug, Clone)]
pub struct TaskLoopContext {
pub session_id: String,
pub items: Vec<TaskLoopItem>,
pub active_item_id: Option<String>,
pub current_round: u32,
pub max_rounds: u32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TaskLoopItem {
pub id: String,
pub description: String,
pub status: TaskItemStatus,
pub depends_on: Vec<String>,
pub notes: String,
pub active_form: Option<String>,
pub parent_id: Option<String>,
pub phase: TaskPhase,
pub priority: TaskPriority,
pub completion_criteria: Vec<String>,
pub evidence: Vec<TaskEvidence>,
pub blockers: Vec<TaskBlocker>,
pub transitions: Vec<TaskTransition>,
pub tool_calls: Vec<ToolCallRecord>,
pub started_at_round: Option<u32>,
pub completed_at_round: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRecord {
pub round: u32,
pub tool_name: String,
pub success: bool,
pub timestamp: DateTime<Utc>,
}
impl TaskLoopContext {
pub fn is_all_completed(&self) -> bool {
!self.items.is_empty()
&& self
.items
.iter()
.all(|item| matches!(item.status, TaskItemStatus::Completed))
}
fn completed_item_ids(&self) -> HashSet<String> {
self.items
.iter()
.filter(|item| matches!(item.status, TaskItemStatus::Completed))
.map(|item| item.id.clone())
.collect()
}
fn unresolved_dependencies(&self, depends_on: &[String]) -> Vec<String> {
let completed = self.completed_item_ids();
depends_on
.iter()
.filter(|dependency| !completed.contains(*dependency))
.cloned()
.collect()
}
fn append_item_notes(item: &mut TaskLoopItem, note: &str) {
let note = note.trim();
if note.is_empty() {
return;
}
if !item.notes.is_empty() {
item.notes.push('\n');
}
item.notes.push_str(note);
}
fn transition_item(
item: &mut TaskLoopItem,
status: TaskItemStatus,
reason: Option<&str>,
round: Option<u32>,
) -> bool {
let reason = reason.map(str::trim).filter(|value| !value.is_empty());
if item.status == status {
if let Some(reason) = reason {
Self::append_item_notes(item, reason);
}
return false;
}
let transition = TaskTransition {
from_status: item.status.clone(),
to_status: status.clone(),
reason: reason.map(ToOwned::to_owned),
round,
changed_at: Utc::now(),
};
item.status = status.clone();
if matches!(status, TaskItemStatus::InProgress) && item.started_at_round.is_none() {
item.started_at_round = round;
}
if matches!(status, TaskItemStatus::Completed) {
item.completed_at_round = round;
}
if let Some(reason) = transition.reason.as_deref() {
Self::append_item_notes(item, reason);
}
item.transitions.push(transition);
true
}
fn add_item_blocker(item: &mut TaskLoopItem, blocker: TaskBlocker) {
if blocker.summary.trim().is_empty() {
return;
}
if item.blockers.iter().any(|existing| {
existing.kind == blocker.kind
&& existing.summary == blocker.summary
&& existing.waiting_on == blocker.waiting_on
}) {
return;
}
item.blockers.push(blocker);
}
fn push_item_evidence(item: &mut TaskLoopItem, evidence: TaskEvidence) {
if evidence.summary.trim().is_empty() {
return;
}
item.evidence.push(evidence);
}
}
#[cfg(test)]
mod tests;