Skip to main content

codetether_agent/ralph/
types.rs

1//! Ralph types - PRD and state structures
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// A user story in the PRD
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct UserStory {
9    /// Unique identifier (e.g., "US-001")
10    pub id: String,
11    
12    /// Short title
13    pub title: String,
14    
15    /// Full description
16    pub description: String,
17    
18    /// Acceptance criteria
19    #[serde(default)]
20    pub acceptance_criteria: Vec<String>,
21    
22    /// Whether this story passes all tests
23    #[serde(default)]
24    pub passes: bool,
25    
26    /// Story priority (1=highest)
27    #[serde(default = "default_priority")]
28    pub priority: u8,
29    
30    /// Dependencies on other story IDs
31    #[serde(default)]
32    pub depends_on: Vec<String>,
33    
34    /// Estimated complexity (1-5)
35    #[serde(default = "default_complexity")]
36    pub complexity: u8,
37}
38
39fn default_priority() -> u8 { 1 }
40fn default_complexity() -> u8 { 3 }
41
42/// The full PRD structure
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Prd {
45    /// Project name
46    pub project: String,
47    
48    /// Feature being implemented
49    pub feature: String,
50    
51    /// Git branch name for this PRD
52    #[serde(default)]
53    pub branch_name: String,
54    
55    /// Version of the PRD format
56    #[serde(default = "default_version")]
57    pub version: String,
58    
59    /// User stories to implement
60    #[serde(default)]
61    pub user_stories: Vec<UserStory>,
62    
63    /// Technical requirements
64    #[serde(default)]
65    pub technical_requirements: Vec<String>,
66    
67    /// Quality checks to run
68    #[serde(default)]
69    pub quality_checks: QualityChecks,
70    
71    /// Created timestamp
72    #[serde(default)]
73    pub created_at: String,
74    
75    /// Last updated timestamp
76    #[serde(default)]
77    pub updated_at: String,
78}
79
80fn default_version() -> String { "1.0".to_string() }
81
82/// Quality checks configuration
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct QualityChecks {
85    /// Command to run type checking
86    #[serde(default)]
87    pub typecheck: Option<String>,
88    
89    /// Command to run tests
90    #[serde(default)]
91    pub test: Option<String>,
92    
93    /// Command to run linting
94    #[serde(default)]
95    pub lint: Option<String>,
96    
97    /// Command to run build
98    #[serde(default)]
99    pub build: Option<String>,
100}
101
102impl Prd {
103    /// Load a PRD from a JSON file
104    pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
105        let content = tokio::fs::read_to_string(path).await?;
106        let prd: Prd = serde_json::from_str(&content)?;
107        Ok(prd)
108    }
109    
110    /// Save the PRD to a JSON file
111    pub async fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
112        let content = serde_json::to_string_pretty(self)?;
113        tokio::fs::write(path, content).await?;
114        Ok(())
115    }
116    
117    /// Get the next story to work on (not passed, dependencies met)
118    pub fn next_story(&self) -> Option<&UserStory> {
119        self.user_stories
120            .iter()
121            .filter(|s| !s.passes)
122            .filter(|s| self.dependencies_met(&s.depends_on))
123            .min_by_key(|s| (s.priority, s.complexity))
124    }
125    
126    /// Check if all dependencies are met (all passed)
127    fn dependencies_met(&self, deps: &[String]) -> bool {
128        deps.iter().all(|dep_id| {
129            self.user_stories
130                .iter()
131                .find(|s| s.id == *dep_id)
132                .map(|s| s.passes)
133                .unwrap_or(true)
134        })
135    }
136    
137    /// Get count of passed stories
138    pub fn passed_count(&self) -> usize {
139        self.user_stories.iter().filter(|s| s.passes).count()
140    }
141    
142    /// Check if all stories are complete
143    pub fn is_complete(&self) -> bool {
144        self.user_stories.iter().all(|s| s.passes)
145    }
146    
147    /// Mark a story as passed
148    pub fn mark_passed(&mut self, story_id: &str) {
149        if let Some(story) = self.user_stories.iter_mut().find(|s| s.id == *story_id) {
150            story.passes = true;
151        }
152    }
153}
154
155/// Ralph execution state
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct RalphState {
158    /// The PRD being worked on
159    pub prd: Prd,
160    
161    /// Current iteration number
162    pub current_iteration: usize,
163    
164    /// Maximum allowed iterations
165    pub max_iterations: usize,
166    
167    /// Current status
168    pub status: RalphStatus,
169    
170    /// Progress log entries
171    #[serde(default)]
172    pub progress_log: Vec<ProgressEntry>,
173    
174    /// Path to the PRD file
175    pub prd_path: PathBuf,
176    
177    /// Working directory
178    pub working_dir: PathBuf,
179}
180
181/// Ralph execution status
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "snake_case")]
184pub enum RalphStatus {
185    Pending,
186    Running,
187    Completed,
188    MaxIterations,
189    Stopped,
190    QualityFailed,
191}
192
193/// A progress log entry
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct ProgressEntry {
196    /// Story ID being worked on
197    pub story_id: String,
198    
199    /// Iteration number
200    pub iteration: usize,
201    
202    /// Status of this attempt
203    pub status: String,
204    
205    /// What was learned
206    #[serde(default)]
207    pub learnings: Vec<String>,
208    
209    /// Files changed
210    #[serde(default)]
211    pub files_changed: Vec<String>,
212    
213    /// Timestamp
214    pub timestamp: String,
215}
216
217/// Ralph configuration
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct RalphConfig {
220    /// Path to prd.json
221    #[serde(default = "default_prd_path")]
222    pub prd_path: String,
223    
224    /// Maximum iterations
225    #[serde(default = "default_max_iterations")]
226    pub max_iterations: usize,
227    
228    /// Path to progress.txt
229    #[serde(default = "default_progress_path")]
230    pub progress_path: String,
231    
232    /// Whether to auto-commit changes
233    #[serde(default = "default_auto_commit")]
234    pub auto_commit: bool,
235    
236    /// Whether to run quality checks
237    #[serde(default = "default_quality_checks_enabled")]
238    pub quality_checks_enabled: bool,
239    
240    /// Model to use for iterations
241    #[serde(default)]
242    pub model: Option<String>,
243    
244    /// Whether to use RLM for progress compression
245    #[serde(default)]
246    pub use_rlm: bool,
247}
248
249fn default_prd_path() -> String { "prd.json".to_string() }
250fn default_max_iterations() -> usize { 10 }
251fn default_progress_path() -> String { "progress.txt".to_string() }
252fn default_auto_commit() -> bool { false }
253fn default_quality_checks_enabled() -> bool { true }
254
255impl Default for RalphConfig {
256    fn default() -> Self {
257        Self {
258            prd_path: default_prd_path(),
259            max_iterations: default_max_iterations(),
260            progress_path: default_progress_path(),
261            auto_commit: default_auto_commit(),
262            quality_checks_enabled: default_quality_checks_enabled(),
263            model: None,
264            use_rlm: false,
265        }
266    }
267}