Skip to main content

git_internal/internal/object/
task.rs

1//! AI Task Definition
2//!
3//! A `Task` represents a unit of work to be performed by an AI agent.
4//! It serves as the root of the AI workflow, defining intent, constraints, and success criteria.
5//!
6//! # Lifecycle
7//!
8//! 1. **Draft**: Initial state. Task is being defined.
9//! 2. **Running**: An agent (via a `Run` object) has started working on it.
10//! 3. **Done**: Work is completed and verified.
11//! 4. **Failed**: Work could not be completed.
12//! 5. **Cancelled**: User aborted the task.
13//!
14//! # Relationships
15//!
16//! - **Parent**: None (Root object).
17//! - **Children**: `Run` (1-to-many). A task can have multiple runs (retries).
18//! - **Dependencies**: Can depend on other Tasks via `dependencies`.
19//!
20//! # Example
21//!
22//! ```rust
23//! use git_internal::internal::object::task::{Task, GoalType};
24//! use git_internal::internal::object::types::ActorRef;
25//! use uuid::Uuid;
26//!
27//! let repo_id = Uuid::new_v4();
28//! let actor = ActorRef::human("user").unwrap();
29//! let mut task = Task::new(repo_id, actor, "Refactor Login", Some(GoalType::Refactor)).unwrap();
30//!
31//! task.add_constraint("Must use JWT");
32//! task.add_acceptance_criterion("All tests pass");
33//! ```
34
35use std::{fmt, str::FromStr};
36
37use serde::{Deserialize, Serialize};
38use uuid::Uuid;
39
40use crate::{
41    errors::GitError,
42    hash::ObjectHash,
43    internal::object::{
44        ObjectTrait,
45        types::{ActorRef, Header, ObjectType},
46    },
47};
48
49/// Task lifecycle status.
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum TaskStatus {
53    /// Initial state, definition in progress.
54    Draft,
55    /// Agent is actively working on this task.
56    Running,
57    /// Task completed successfully.
58    Done,
59    /// Task failed to complete.
60    Failed,
61    /// Task was cancelled by user.
62    Cancelled,
63}
64
65impl TaskStatus {
66    pub fn as_str(&self) -> &'static str {
67        match self {
68            TaskStatus::Draft => "draft",
69            TaskStatus::Running => "running",
70            TaskStatus::Done => "done",
71            TaskStatus::Failed => "failed",
72            TaskStatus::Cancelled => "cancelled",
73        }
74    }
75}
76
77impl fmt::Display for TaskStatus {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.as_str())
80    }
81}
82
83/// Task goal category.
84///
85/// Helps agents understand the nature of the work.
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "snake_case")]
88pub enum GoalType {
89    Feature,
90    Bugfix,
91    Refactor,
92    Docs,
93    Perf,
94    Test,
95    Chore,
96    Build,
97    Ci,
98    Style,
99}
100
101impl GoalType {
102    pub fn as_str(&self) -> &'static str {
103        match self {
104            GoalType::Feature => "feature",
105            GoalType::Bugfix => "bugfix",
106            GoalType::Refactor => "refactor",
107            GoalType::Docs => "docs",
108            GoalType::Perf => "perf",
109            GoalType::Test => "test",
110            GoalType::Chore => "chore",
111            GoalType::Build => "build",
112            GoalType::Ci => "ci",
113            GoalType::Style => "style",
114        }
115    }
116}
117
118impl fmt::Display for GoalType {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "{}", self.as_str())
121    }
122}
123
124impl FromStr for GoalType {
125    type Err = String;
126
127    fn from_str(value: &str) -> Result<Self, Self::Err> {
128        match value {
129            "feature" => Ok(GoalType::Feature),
130            "bugfix" => Ok(GoalType::Bugfix),
131            "refactor" => Ok(GoalType::Refactor),
132            "docs" => Ok(GoalType::Docs),
133            "perf" => Ok(GoalType::Perf),
134            "test" => Ok(GoalType::Test),
135            "chore" => Ok(GoalType::Chore),
136            "build" => Ok(GoalType::Build),
137            "ci" => Ok(GoalType::Ci),
138            "style" => Ok(GoalType::Style),
139            _ => Err(format!("Invalid goal_type: {}", value)),
140        }
141    }
142}
143
144/// Task object describing intent and constraints.
145/// Typically created first, then referenced by Run objects.
146///
147/// See module documentation for lifecycle details.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct Task {
150    #[serde(flatten)]
151    header: Header,
152    title: String,
153    description: Option<String>,
154    goal_type: Option<GoalType>,
155    #[serde(default)]
156    constraints: Vec<String>,
157    #[serde(default)]
158    acceptance_criteria: Vec<String>,
159    requested_by: Option<ActorRef>,
160    #[serde(default)]
161    dependencies: Vec<Uuid>,
162    status: TaskStatus,
163}
164
165impl Task {
166    /// Create a new Task.
167    ///
168    /// # Arguments
169    /// * `repo_id` - Repository UUID
170    /// * `created_by` - Actor creating the task
171    /// * `title` - Short summary of the task
172    /// * `goal_type` - Optional classification (Feature, Bugfix, etc.)
173    pub fn new(
174        repo_id: Uuid,
175        created_by: ActorRef,
176        title: impl Into<String>,
177        goal_type: Option<GoalType>,
178    ) -> Result<Self, String> {
179        Ok(Self {
180            header: Header::new(ObjectType::Task, repo_id, created_by)?,
181            title: title.into(),
182            description: None,
183            goal_type,
184            constraints: Vec::new(),
185            acceptance_criteria: Vec::new(),
186            requested_by: None,
187            dependencies: Vec::new(),
188            status: TaskStatus::Draft,
189        })
190    }
191
192    pub fn header(&self) -> &Header {
193        &self.header
194    }
195
196    pub fn title(&self) -> &str {
197        &self.title
198    }
199
200    pub fn description(&self) -> Option<&str> {
201        self.description.as_deref()
202    }
203
204    pub fn goal_type(&self) -> Option<&GoalType> {
205        self.goal_type.as_ref()
206    }
207
208    pub fn constraints(&self) -> &[String] {
209        &self.constraints
210    }
211
212    pub fn acceptance_criteria(&self) -> &[String] {
213        &self.acceptance_criteria
214    }
215
216    pub fn requested_by(&self) -> Option<&ActorRef> {
217        self.requested_by.as_ref()
218    }
219
220    pub fn dependencies(&self) -> &[Uuid] {
221        &self.dependencies
222    }
223
224    pub fn status(&self) -> &TaskStatus {
225        &self.status
226    }
227
228    pub fn set_description(&mut self, description: Option<String>) {
229        self.description = description;
230    }
231
232    pub fn add_constraint(&mut self, constraint: impl Into<String>) {
233        self.constraints.push(constraint.into());
234    }
235
236    pub fn add_acceptance_criterion(&mut self, criterion: impl Into<String>) {
237        self.acceptance_criteria.push(criterion.into());
238    }
239
240    pub fn set_requested_by(&mut self, requested_by: Option<ActorRef>) {
241        self.requested_by = requested_by;
242    }
243
244    pub fn add_dependency(&mut self, task_id: Uuid) {
245        self.dependencies.push(task_id);
246    }
247
248    pub fn set_status(&mut self, status: TaskStatus) {
249        self.status = status;
250    }
251}
252
253impl fmt::Display for Task {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(f, "Task: {}", self.header.object_id())
256    }
257}
258
259impl ObjectTrait for Task {
260    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
261    where
262        Self: Sized,
263    {
264        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
265    }
266
267    fn get_type(&self) -> ObjectType {
268        ObjectType::Task
269    }
270
271    fn get_size(&self) -> usize {
272        serde_json::to_vec(self).map(|v| v.len()).unwrap_or(0)
273    }
274
275    fn to_data(&self) -> Result<Vec<u8>, GitError> {
276        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::internal::object::types::ActorKind;
284
285    #[test]
286    fn test_task_creation() {
287        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
288        let actor = ActorRef::human("jackie").expect("actor");
289        let mut task = Task::new(repo_id, actor, "Fix bug", Some(GoalType::Bugfix)).expect("task");
290
291        // Test dependencies
292        let dep_id = Uuid::from_u128(0x00000000000000000000000000000001);
293        task.add_dependency(dep_id);
294
295        assert_eq!(task.header().object_type(), &ObjectType::Task);
296        assert_eq!(task.status(), &TaskStatus::Draft);
297        assert_eq!(task.goal_type(), Some(&GoalType::Bugfix));
298        assert_eq!(task.dependencies().len(), 1);
299        assert_eq!(task.dependencies()[0], dep_id);
300    }
301
302    #[test]
303    fn test_task_goal_type_optional() {
304        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
305        let actor = ActorRef::human("jackie").expect("actor");
306        let task = Task::new(repo_id, actor, "Write docs", None).expect("task");
307
308        assert!(task.goal_type().is_none());
309    }
310
311    #[test]
312    fn test_task_requested_by() {
313        let repo_id = Uuid::from_u128(0x0123456789abcdef0123456789abcdef);
314        let actor = ActorRef::human("jackie").expect("actor");
315        let mut task =
316            Task::new(repo_id, actor.clone(), "Fix bug", Some(GoalType::Bugfix)).expect("task");
317
318        task.set_requested_by(Some(ActorRef::mcp_client("vscode-client").expect("actor")));
319
320        assert!(task.requested_by().is_some());
321        assert_eq!(task.requested_by().unwrap().kind(), &ActorKind::McpClient);
322    }
323}