An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
7#[serde(rename_all = "snake_case")]
8pub enum TaskStatus {
9 Pending,
10 InProgress,
11 Complete,
12 Blocked,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Task {
17 pub id: String,
18 pub title: String,
19 pub description: String,
20 pub acceptance_criteria: Vec<String>,
21 pub status: TaskStatus,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub blocked_reason: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub completed_at: Option<DateTime<Utc>>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Spec {
30 pub name: String,
31 pub description: String,
32 pub branch_name: String,
33 pub created_at: DateTime<Utc>,
34 pub tasks: Vec<Task>,
35 pub learnings: Vec<String>,
36}
37
38impl Spec {
39 /// Load a spec from a JSON file
40 pub fn load(path: impl AsRef<Path>) -> anyhow::Result<Self> {
41 let content = fs::read_to_string(path)?;
42 let spec = serde_json::from_str(&content)?;
43 Ok(spec)
44 }
45
46 /// Save the spec to a JSON file
47 pub fn save(&self, path: impl AsRef<Path>) -> anyhow::Result<()> {
48 let path = path.as_ref();
49
50 // Create parent directories if they don't exist
51 if let Some(parent) = path.parent() {
52 fs::create_dir_all(parent)?;
53 }
54
55 let json = serde_json::to_string_pretty(self)?;
56 fs::write(path, json)?;
57 Ok(())
58 }
59
60 /// Find the next task that is pending (not in progress, complete, or blocked)
61 pub fn find_next_task(&self) -> Option<&Task> {
62 self.tasks
63 .iter()
64 .find(|task| task.status == TaskStatus::Pending)
65 }
66
67 /// Find a task by ID and return a mutable reference
68 pub fn find_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
69 self.tasks.iter_mut().find(|task| task.id == task_id)
70 }
71
72 /// Add a learning to the spec
73 pub fn add_learning(&mut self, learning: String) {
74 self.learnings.push(learning);
75 }
76}