claimd 0.1.0

Concurrent todo list CLI for multi-agent AI workflows
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use std::fmt;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    New,
    InProgress,
    PrOpen,
    PrChangesRequested,
    Done,
    Incomplete,
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::New => write!(f, "New"),
            Status::InProgress => write!(f, "InProgress"),
            Status::PrOpen => write!(f, "PrOpen"),
            Status::PrChangesRequested => write!(f, "PrChangesRequested"),
            Status::Done => write!(f, "Done"),
            Status::Incomplete => write!(f, "Incomplete"),
        }
    }
}

impl std::str::FromStr for Status {
    type Err = String;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "new" => Ok(Status::New),
            "in_progress" | "inprogress" | "in-progress" => Ok(Status::InProgress),
            "pr_open" | "propen" | "pr-open" => Ok(Status::PrOpen),
            "pr_changes_requested" | "prchangesrequested" | "pr-changes-requested" => Ok(Status::PrChangesRequested),
            "done" => Ok(Status::Done),
            "incomplete" => Ok(Status::Incomplete),
            _ => Err(format!("Unknown status: '{s}'. Valid: new, in_progress, pr_open, pr_changes_requested, done, incomplete")),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
    pub id: Uuid,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    pub status: Status,
    pub priority: u8,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub claimed_by: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pr_url: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub previously_claimed_by: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub link: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub author: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub depends_on: Vec<Uuid>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub depends_on_completed: Vec<Uuid>,
}

impl TodoItem {
    pub fn new(
        title: String,
        description: Option<String>,
        priority: u8,
        tags: Vec<String>,
        link: Option<String>,
        source: Option<String>,
        author: Option<String>,
        depends_on: Vec<Uuid>,
    ) -> Self {
        let now = Utc::now();
        TodoItem {
            id: Uuid::new_v4(),
            title,
            description,
            status: Status::New,
            priority,
            created_at: now,
            updated_at: now,
            claimed_by: None,
            pr_url: None,
            previously_claimed_by: Vec::new(),
            link,
            source,
            author,
            tags,
            depends_on,
            depends_on_completed: Vec::new(),
        }
    }

    pub fn short_id(&self) -> String {
        self.id.to_string()[..8].to_string()
    }

    pub fn has_pending_deps(&self) -> bool {
        !self.depends_on.is_empty()
    }
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TodoList {
    pub items: Vec<TodoItem>,
}

fn default_active() -> bool { true }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMeta {
    #[serde(default = "default_active")]
    pub active: bool,
}

impl Default for ProjectMeta {
    fn default() -> Self {
        ProjectMeta { active: true }
    }
}