mod graph;
mod input;
mod intrinsic;
mod render;
mod store;
#[cfg(test)]
mod tests;
mod types;
use std::{io, path::Path};
use serde_json::Value;
use thiserror::Error;
use crate::runtime::{RuntimeError, store::TaskStore};
pub(crate) use intrinsic::TaskIntrinsicTool;
pub(crate) const TASK_REMINDER_TEXT: &str = "Reminder: use task_create, task_claim, task_update, task_list, or task_get only for persisted project-task tracking. Do not use task tools to manage persistent teammates or team protocol flows.";
pub(crate) use graph::has_unfinished_tasks;
pub use types::{TaskItem, TaskStatus};
pub(crate) fn deserialize_present_nullable_string<'de, D>(
deserializer: D,
) -> Result<Option<Option<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
<Option<String> as serde::Deserialize>::deserialize(deserializer).map(Some)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TaskAccess<'a> {
Lead,
LeadClaimant(&'a str),
Teammate(&'a str),
}
#[derive(Debug, Error)]
pub(crate) enum TaskError {
#[error("Task storage I/O failed: {0}")]
Io(#[from] io::Error),
#[error("Task serialization failed: {0}")]
Serde(#[from] serde_json::Error),
#[error("Task validation failed: {0}")]
Validation(String),
}
pub(crate) fn execute_with_store<S: TaskStore + ?Sized>(
store: &S,
tool: &TaskIntrinsicTool,
input: Value,
namespace: &Path,
access: TaskAccess<'_>,
) -> Result<String, String> {
match tool {
TaskIntrinsicTool::Create => {
let parsed = input::parse_task_create_input(input)?;
mutate_store_tasks(store, namespace, move |tasks| {
let task_id = tasks.iter().map(|task| task.id).max().unwrap_or(0) + 1;
tasks.push(TaskItem {
id: task_id,
subject: parsed.subject.trim().to_string(),
description: parsed.description.clone(),
status: TaskStatus::Pending,
blocked_by: Vec::new(),
blocks: Vec::new(),
owner: parsed.owner.clone(),
working_directory: parsed.working_directory.clone(),
});
for blocker_id in &parsed.blocked_by {
graph::add_dependency(tasks, *blocker_id, task_id)
.map_err(|error| error.to_string())?;
}
render::serialize_pretty(
graph::find_task(tasks, task_id).map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())
})
}
TaskIntrinsicTool::Claim => {
let parsed = input::parse_task_claim_input(input)?;
let owner = access
.actor_name()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "Only named teammates can claim tasks".to_string())?
.trim()
.to_string();
mutate_store_tasks(store, namespace, move |tasks| {
let claimed = match parsed.task_id {
Some(task_id) => {
let task = store::find_task_mut(tasks, task_id)
.map_err(|error| error.to_string())?;
store::validate_claimable(task, &owner)
.map_err(|error| error.to_string())?;
task.owner = owner.clone();
task.clone()
}
None => {
let task = tasks
.iter_mut()
.find(|task| store::is_claimable(task))
.ok_or_else(|| {
"No ready unowned tasks are available to claim".to_string()
})?;
task.owner = owner.clone();
task.clone()
}
};
render::serialize_pretty(&claimed).map_err(|error| error.to_string())
})
}
TaskIntrinsicTool::Update => {
let parsed = input::parse_task_update_input(input)?;
mutate_store_tasks(store, namespace, move |tasks| {
let task_id = parsed.task_id;
let original_status = graph::find_task(tasks, task_id)
.map_err(|error| error.to_string())?
.status
.clone();
store::validate_update_access(
graph::find_task(tasks, task_id).map_err(|error| error.to_string())?,
&parsed,
access,
)
.map_err(|error| error.to_string())?;
{
let task =
store::find_task_mut(tasks, task_id).map_err(|error| error.to_string())?;
if let Some(subject) = parsed.subject.clone() {
task.subject = subject.trim().to_string();
}
if let Some(description) = parsed.description.clone() {
task.description = description;
}
if let Some(owner) = parsed.owner.clone() {
task.owner = owner;
}
if let Some(working_directory) = parsed.working_directory.clone() {
task.working_directory = working_directory;
}
}
for blocker_id in &parsed.add_blocked_by {
graph::add_dependency(tasks, *blocker_id, task_id)
.map_err(|error| error.to_string())?;
}
for blocker_id in &parsed.remove_blocked_by {
graph::remove_dependency(tasks, *blocker_id, task_id)
.map_err(|error| error.to_string())?;
}
for dependent_id in &parsed.add_blocks {
graph::add_dependency(tasks, task_id, *dependent_id)
.map_err(|error| error.to_string())?;
}
for dependent_id in &parsed.remove_blocks {
graph::remove_dependency(tasks, task_id, *dependent_id)
.map_err(|error| error.to_string())?;
}
let mut unblocked = Vec::new();
let mut reblocked = Vec::new();
if let Some(status) = parsed.status.clone() {
graph::apply_status_change(
tasks,
task_id,
original_status,
status,
&mut unblocked,
&mut reblocked,
)
.map_err(|error| error.to_string())?;
} else {
store::validate_unblocked_status(
graph::find_task(tasks, task_id).map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
}
graph::sort_tasks(&mut unblocked);
graph::sort_tasks(&mut reblocked);
render::serialize_pretty(&render::TaskUpdateOutput {
task: graph::find_task(tasks, task_id)
.map_err(|error| error.to_string())?
.clone(),
unblocked,
reblocked,
})
.map_err(|error| error.to_string())
})
}
TaskIntrinsicTool::Get => {
let parsed = input::parse_task_get_input(input)?;
let tasks = load_store_tasks(store, namespace)?;
render::serialize_pretty(
graph::find_task(&tasks, parsed.task_id).map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())
}
TaskIntrinsicTool::List => {
input::parse_task_list_input(input)?;
let tasks = load_store_tasks(store, namespace)?;
let mut ready = Vec::new();
let mut blocked = Vec::new();
let mut in_progress = Vec::new();
let mut completed = Vec::new();
for task in &tasks {
match task.status {
TaskStatus::Pending if task.blocked_by.is_empty() => ready.push(task.clone()),
TaskStatus::Pending => blocked.push(task.clone()),
TaskStatus::InProgress => in_progress.push(task.clone()),
TaskStatus::Completed => completed.push(task.clone()),
}
}
render::serialize_pretty(&render::TaskListOutput {
tasks,
ready,
blocked,
in_progress,
completed,
})
.map_err(|error| error.to_string())
}
}
}
impl<'a> TaskAccess<'a> {
pub(crate) fn actor_name(self) -> Option<&'a str> {
match self {
Self::Lead => None,
Self::LeadClaimant(name) | Self::Teammate(name) => Some(name),
}
}
}
fn load_store_tasks<S: TaskStore + ?Sized>(
store: &S,
namespace: &Path,
) -> Result<Vec<TaskItem>, String> {
store
.load_tasks(namespace)
.map_err(|error| format!("Task storage failed: {error}"))
}
fn mutate_store_tasks<S: TaskStore + ?Sized>(
store: &S,
namespace: &Path,
mut operation: impl FnMut(&mut Vec<TaskItem>) -> Result<String, String>,
) -> Result<String, String> {
let mut output = None;
let mut task_error = None;
let result = {
let mut mutation = |tasks: &mut Vec<TaskItem>| match operation(tasks) {
Ok(value) => {
output = Some(value);
Ok(())
}
Err(error) => {
task_error = Some(error.clone());
Err(RuntimeError::InvalidTask(error))
}
};
store.mutate(namespace, &mut mutation)
};
match result {
Ok(()) => output.ok_or_else(|| "Task mutation did not produce a result".to_string()),
Err(_) if task_error.is_some() => Err(task_error.expect("checked above")),
Err(error) => Err(store_error(error)),
}
}
fn store_error(error: crate::runtime::RuntimeError) -> String {
format!("Task storage failed: {error}")
}