tli 0.1.2

Fast file-backed task tracker for humans, hooks, and AI agents.
Documentation
use std::collections::BTreeMap;
use std::fmt::{self, Display};

use chrono::{DateTime, Utc};
use clap::ValueEnum;
use serde::{Deserialize, Serialize};

pub const STORE_SCHEMA_VERSION: u32 = 3;

#[derive(
    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, ValueEnum,
)]
#[serde(rename_all = "snake_case")]
#[value(rename_all = "snake_case")]
pub enum TaskStatus {
    Todo,
    Active,
    Checkpoint,
    Blocked,
    Review,
    Done,
}

impl TaskStatus {
    pub fn sort_rank(self) -> u8 {
        match self {
            Self::Active => 0,
            Self::Todo => 1,
            Self::Checkpoint => 2,
            Self::Blocked => 3,
            Self::Review => 4,
            Self::Done => 5,
        }
    }
}

impl Display for TaskStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let value = match self {
            Self::Todo => "todo",
            Self::Active => "active",
            Self::Checkpoint => "checkpoint",
            Self::Blocked => "blocked",
            Self::Review => "review",
            Self::Done => "done",
        };
        f.write_str(value)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum TaskSchedule {
    Interval { every_minutes: u32 },
    Cron { expression: String },
}

impl Display for TaskSchedule {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Interval { every_minutes } => write!(f, "every {every_minutes}m"),
            Self::Cron { expression } => write!(f, "cron {expression}"),
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskContinuation {
    #[serde(default)]
    pub next_step: Option<String>,
    #[serde(default)]
    pub next_task: Option<String>,
}

impl TaskContinuation {
    pub fn is_empty(&self) -> bool {
        self.next_step.is_none() && self.next_task.is_none()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskSummary {
    pub id: String,
    pub title: String,
    pub status: TaskStatus,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    #[serde(default)]
    pub ready_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub schedule: Option<TaskSchedule>,
    #[serde(default)]
    pub labels: Vec<String>,
    #[serde(default)]
    pub depends_on: Vec<String>,
    #[serde(default)]
    pub continuation: TaskContinuation,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskNote {
    pub at: DateTime<Utc>,
    pub text: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRecord {
    #[serde(flatten)]
    pub summary: TaskSummary,
    #[serde(default)]
    pub summary_text: Option<String>,
    #[serde(default)]
    pub blocked_reason: Option<String>,
    #[serde(default)]
    pub completed_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub completed_note: Option<String>,
    #[serde(default)]
    pub active_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub checkpointed_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub review_requested_at: Option<DateTime<Utc>>,
    #[serde(default)]
    pub notes: Vec<TaskNote>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskEventKind {
    Created,
    Started,
    ScheduleUpdated,
    Checkpointed,
    Blocked,
    ReviewRequested,
    Completed,
    NoteAdded,
    DependencyAdded,
    DependencyRemoved,
    SubtaskAdded,
    SubtaskRemoved,
}

impl Display for TaskEventKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let value = match self {
            Self::Created => "created",
            Self::Started => "started",
            Self::ScheduleUpdated => "schedule_updated",
            Self::Checkpointed => "checkpointed",
            Self::Blocked => "blocked",
            Self::ReviewRequested => "review_requested",
            Self::Completed => "completed",
            Self::NoteAdded => "note_added",
            Self::DependencyAdded => "dependency_added",
            Self::DependencyRemoved => "dependency_removed",
            Self::SubtaskAdded => "subtask_added",
            Self::SubtaskRemoved => "subtask_removed",
        };
        f.write_str(value)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskEvent {
    pub at: DateTime<Utc>,
    pub task_id: String,
    pub kind: TaskEventKind,
    #[serde(default)]
    pub status: Option<TaskStatus>,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskDetail {
    pub task: TaskRecord,
    #[serde(default)]
    pub dependencies: Vec<TaskSummary>,
    #[serde(default)]
    pub missing_dependencies: Vec<String>,
    #[serde(default)]
    pub blocked_by: Vec<TaskSummary>,
    pub ready: bool,
    #[serde(default)]
    pub next: TaskContinuation,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadyTask {
    #[serde(flatten)]
    pub task: TaskSummary,
    pub ready: bool,
    pub dependency_count: usize,
    #[serde(default)]
    pub missing_dependencies: Vec<String>,
    #[serde(default)]
    pub next: TaskContinuation,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateTask {
    #[serde(flatten)]
    pub task: TaskSummary,
    pub ready: bool,
    pub dependency_count: usize,
    #[serde(default)]
    pub next: TaskContinuation,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateCounts {
    pub todo: usize,
    pub active: usize,
    pub checkpoint: usize,
    pub blocked: usize,
    pub review: usize,
    pub done: usize,
    pub ready: usize,
    pub pending_dependencies: usize,
    pub handoff: usize,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateSnapshot {
    #[serde(default)]
    pub counts: StateCounts,
    #[serde(default)]
    pub ready: Vec<StateTask>,
    #[serde(default)]
    pub pending_dependencies: Vec<StateTask>,
    #[serde(default)]
    pub active: Vec<StateTask>,
    #[serde(default)]
    pub blocked: Vec<StateTask>,
    #[serde(default)]
    pub checkpoint: Vec<StateTask>,
    #[serde(default)]
    pub review: Vec<StateTask>,
    #[serde(default)]
    pub handoff: Vec<StateTask>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreIndex {
    pub schema_version: u32,
    pub tasks: BTreeMap<String, TaskSummary>,
}

impl Default for StoreIndex {
    fn default() -> Self {
        Self {
            schema_version: STORE_SCHEMA_VERSION,
            tasks: BTreeMap::new(),
        }
    }
}