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, alias = "dependencies")]
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 {
40    1
41}
42fn default_complexity() -> u8 {
43    3
44}
45
46/// The full PRD structure
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Prd {
49    /// Project name
50    pub project: String,
51
52    /// Feature being implemented
53    pub feature: String,
54
55    /// Git branch name for this PRD
56    #[serde(default)]
57    pub branch_name: String,
58
59    /// Version of the PRD format
60    #[serde(default = "default_version")]
61    pub version: String,
62
63    /// User stories to implement
64    #[serde(default)]
65    pub user_stories: Vec<UserStory>,
66
67    /// Technical requirements
68    #[serde(default)]
69    pub technical_requirements: Vec<String>,
70
71    /// Quality checks to run
72    #[serde(default)]
73    pub quality_checks: QualityChecks,
74
75    /// Created timestamp
76    #[serde(default)]
77    pub created_at: String,
78
79    /// Last updated timestamp
80    #[serde(default)]
81    pub updated_at: String,
82}
83
84fn default_version() -> String {
85    "1.0".to_string()
86}
87
88/// Quality checks configuration
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct QualityChecks {
91    /// Command to run type checking
92    #[serde(default)]
93    pub typecheck: Option<String>,
94
95    /// Command to run tests
96    #[serde(default)]
97    pub test: Option<String>,
98
99    /// Command to run linting
100    #[serde(default)]
101    pub lint: Option<String>,
102
103    /// Command to run build
104    #[serde(default)]
105    pub build: Option<String>,
106}
107
108impl Prd {
109    /// Load a PRD from a JSON file
110    pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
111        let content = tokio::fs::read_to_string(path).await?;
112        let prd: Prd = serde_json::from_str(&content)?;
113        Ok(prd)
114    }
115
116    /// Save the PRD to a JSON file
117    pub async fn save(&self, path: &PathBuf) -> anyhow::Result<()> {
118        let content = serde_json::to_string_pretty(self)?;
119        tokio::fs::write(path, content).await?;
120        Ok(())
121    }
122
123    /// Get the next story to work on (not passed, dependencies met)
124    pub fn next_story(&self) -> Option<&UserStory> {
125        self.user_stories
126            .iter()
127            .filter(|s| !s.passes)
128            .filter(|s| self.dependencies_met(&s.depends_on))
129            .min_by_key(|s| (s.priority, s.complexity))
130    }
131
132    /// Check if all dependencies are met (all passed)
133    fn dependencies_met(&self, deps: &[String]) -> bool {
134        deps.iter().all(|dep_id| {
135            self.user_stories
136                .iter()
137                .find(|s| s.id == *dep_id)
138                .map(|s| s.passes)
139                .unwrap_or(true)
140        })
141    }
142
143    /// Get count of passed stories
144    pub fn passed_count(&self) -> usize {
145        self.user_stories.iter().filter(|s| s.passes).count()
146    }
147
148    /// Check if all stories are complete
149    pub fn is_complete(&self) -> bool {
150        self.user_stories.iter().all(|s| s.passes)
151    }
152
153    /// Mark a story as passed
154    pub fn mark_passed(&mut self, story_id: &str) {
155        if let Some(story) = self.user_stories.iter_mut().find(|s| s.id == *story_id) {
156            story.passes = true;
157        }
158    }
159
160    /// Get all stories ready to be worked on (not passed, dependencies met)
161    pub fn ready_stories(&self) -> Vec<&UserStory> {
162        self.user_stories
163            .iter()
164            .filter(|s| !s.passes)
165            .filter(|s| self.dependencies_met(&s.depends_on))
166            .collect()
167    }
168
169    /// Group stories into parallel execution stages based on dependencies
170    /// Returns a Vec of stages, where each stage is a Vec of stories that can run in parallel
171    pub fn stages(&self) -> Vec<Vec<&UserStory>> {
172        let mut stages: Vec<Vec<&UserStory>> = Vec::new();
173        let mut completed: std::collections::HashSet<String> = self
174            .user_stories
175            .iter()
176            .filter(|s| s.passes)
177            .map(|s| s.id.clone())
178            .collect();
179
180        let mut remaining: Vec<&UserStory> =
181            self.user_stories.iter().filter(|s| !s.passes).collect();
182
183        while !remaining.is_empty() {
184            // Find all stories whose dependencies are met
185            let (ready, not_ready): (Vec<_>, Vec<_>) = remaining
186                .into_iter()
187                .partition(|s| s.depends_on.iter().all(|dep| completed.contains(dep)));
188
189            if ready.is_empty() {
190                // Circular dependency or missing deps - just take remaining
191                if !not_ready.is_empty() {
192                    stages.push(not_ready);
193                }
194                break;
195            }
196
197            // Mark these as "will be completed" for next iteration
198            for story in &ready {
199                completed.insert(story.id.clone());
200            }
201
202            stages.push(ready);
203            remaining = not_ready;
204        }
205
206        stages
207    }
208}
209
210/// Ralph execution state
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct RalphState {
213    /// The PRD being worked on
214    pub prd: Prd,
215
216    /// Current iteration number
217    pub current_iteration: usize,
218
219    /// Maximum allowed iterations
220    pub max_iterations: usize,
221
222    /// Current status
223    pub status: RalphStatus,
224
225    /// Progress log entries
226    #[serde(default)]
227    pub progress_log: Vec<ProgressEntry>,
228
229    /// Path to the PRD file
230    pub prd_path: PathBuf,
231
232    /// Working directory
233    pub working_dir: PathBuf,
234}
235
236/// Ralph execution status
237#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "snake_case")]
239pub enum RalphStatus {
240    Pending,
241    Running,
242    Completed,
243    MaxIterations,
244    Stopped,
245    QualityFailed,
246}
247
248/// A progress log entry
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ProgressEntry {
251    /// Story ID being worked on
252    pub story_id: String,
253
254    /// Iteration number
255    pub iteration: usize,
256
257    /// Status of this attempt
258    pub status: String,
259
260    /// What was learned
261    #[serde(default)]
262    pub learnings: Vec<String>,
263
264    /// Files changed
265    #[serde(default)]
266    pub files_changed: Vec<String>,
267
268    /// Timestamp
269    pub timestamp: String,
270}
271
272/// Ralph configuration
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct RalphConfig {
275    /// Path to prd.json
276    #[serde(default = "default_prd_path")]
277    pub prd_path: String,
278
279    /// Maximum iterations
280    #[serde(default = "default_max_iterations")]
281    pub max_iterations: usize,
282
283    /// Path to progress.txt
284    #[serde(default = "default_progress_path")]
285    pub progress_path: String,
286
287    /// Whether to auto-commit changes
288    #[serde(default = "default_auto_commit")]
289    pub auto_commit: bool,
290
291    /// Whether to run quality checks
292    #[serde(default = "default_quality_checks_enabled")]
293    pub quality_checks_enabled: bool,
294
295    /// Model to use for iterations
296    #[serde(default)]
297    pub model: Option<String>,
298
299    /// Whether to use RLM for progress compression
300    #[serde(default)]
301    pub use_rlm: bool,
302
303    /// Enable parallel story execution
304    #[serde(default = "default_parallel_enabled")]
305    pub parallel_enabled: bool,
306
307    /// Maximum concurrent stories to execute
308    #[serde(default = "default_max_concurrent_stories")]
309    pub max_concurrent_stories: usize,
310
311    /// Use worktree isolation for parallel execution
312    #[serde(default = "default_worktree_enabled")]
313    pub worktree_enabled: bool,
314
315    /// Timeout in seconds per step for story sub-agents (resets on each step)
316    #[serde(default = "default_story_timeout_secs")]
317    pub story_timeout_secs: u64,
318
319    /// Timeout in seconds per step for conflict resolution sub-agents
320    #[serde(default = "default_conflict_timeout_secs")]
321    pub conflict_timeout_secs: u64,
322}
323
324fn default_prd_path() -> String {
325    "prd.json".to_string()
326}
327fn default_max_iterations() -> usize {
328    10
329}
330fn default_progress_path() -> String {
331    "progress.txt".to_string()
332}
333fn default_auto_commit() -> bool {
334    false
335}
336fn default_quality_checks_enabled() -> bool {
337    true
338}
339fn default_parallel_enabled() -> bool {
340    true
341}
342fn default_max_concurrent_stories() -> usize {
343    3
344}
345fn default_worktree_enabled() -> bool {
346    true
347}
348fn default_story_timeout_secs() -> u64 {
349    300
350}
351fn default_conflict_timeout_secs() -> u64 {
352    120
353}
354
355impl Default for RalphConfig {
356    fn default() -> Self {
357        Self {
358            prd_path: default_prd_path(),
359            max_iterations: default_max_iterations(),
360            progress_path: default_progress_path(),
361            auto_commit: default_auto_commit(),
362            quality_checks_enabled: default_quality_checks_enabled(),
363            model: None,
364            use_rlm: false,
365            parallel_enabled: default_parallel_enabled(),
366            max_concurrent_stories: default_max_concurrent_stories(),
367            worktree_enabled: default_worktree_enabled(),
368            story_timeout_secs: default_story_timeout_secs(),
369            conflict_timeout_secs: default_conflict_timeout_secs(),
370        }
371    }
372}