1use crate::agents::AgentRole;
7use crate::checkpoint::execution_history::ExecutionStep;
8use serde::{Deserialize, Serialize};
9use std::path::PathBuf;
10
11use super::event::PipelinePhase;
12
13#[derive(Clone, Serialize, Deserialize, Debug)]
18pub struct PipelineState {
19 pub phase: PipelinePhase,
20 pub previous_phase: Option<PipelinePhase>,
21 pub iteration: u32,
22 pub total_iterations: u32,
23 pub reviewer_pass: u32,
24 pub total_reviewer_passes: u32,
25 pub review_issues_found: bool,
26 pub context_cleaned: bool,
27 pub agent_chain: AgentChainState,
28 pub rebase: RebaseState,
29 pub commit: CommitState,
30 pub execution_history: Vec<ExecutionStep>,
31}
32
33impl PipelineState {
34 pub fn initial(developer_iters: u32, reviewer_reviews: u32) -> Self {
35 let initial_phase = if developer_iters == 0 {
37 if reviewer_reviews == 0 {
39 PipelinePhase::CommitMessage
41 } else {
42 PipelinePhase::Review
43 }
44 } else {
45 PipelinePhase::Planning
46 };
47
48 Self {
49 phase: initial_phase,
50 previous_phase: None,
51 iteration: 0,
52 total_iterations: developer_iters,
53 reviewer_pass: 0,
54 total_reviewer_passes: reviewer_reviews,
55 review_issues_found: false,
56 context_cleaned: false,
57 agent_chain: AgentChainState::initial(),
58 rebase: RebaseState::NotStarted,
59 commit: CommitState::NotStarted,
60 execution_history: Vec::new(),
61 }
62 }
63
64 pub fn is_complete(&self) -> bool {
65 matches!(
66 self.phase,
67 PipelinePhase::Complete | PipelinePhase::Interrupted
68 )
69 }
70
71 pub fn current_head(&self) -> String {
72 self.rebase
73 .current_head()
74 .unwrap_or_else(|| "HEAD".to_string())
75 }
76}
77
78#[derive(Clone, Serialize, Deserialize, Debug)]
85pub struct AgentChainState {
86 pub agents: Vec<String>,
87 pub current_agent_index: usize,
88 pub models_per_agent: Vec<Vec<String>>,
89 pub current_model_index: usize,
90 pub retry_cycle: u32,
91 pub max_cycles: u32,
92 pub current_role: AgentRole,
93}
94
95impl AgentChainState {
96 pub fn initial() -> Self {
97 Self {
98 agents: Vec::new(),
99 current_agent_index: 0,
100 models_per_agent: Vec::new(),
101 current_model_index: 0,
102 retry_cycle: 0,
103 max_cycles: 3,
104 current_role: AgentRole::Developer,
105 }
106 }
107
108 pub fn with_agents(
109 mut self,
110 agents: Vec<String>,
111 models_per_agent: Vec<Vec<String>>,
112 role: AgentRole,
113 ) -> Self {
114 self.agents = agents;
115 self.models_per_agent = models_per_agent;
116 self.current_role = role;
117 self
118 }
119
120 #[doc(hidden)]
121 pub fn with_max_cycles(mut self, max_cycles: u32) -> Self {
122 self.max_cycles = max_cycles;
123 self
124 }
125
126 pub fn current_agent(&self) -> Option<&String> {
127 self.agents.get(self.current_agent_index)
128 }
129
130 #[doc(hidden)]
131 pub fn current_model(&self) -> Option<&String> {
132 self.models_per_agent
133 .get(self.current_agent_index)
134 .and_then(|models| models.get(self.current_model_index))
135 }
136
137 pub fn is_exhausted(&self) -> bool {
138 self.retry_cycle >= self.max_cycles
139 && self.current_agent_index == 0
140 && self.current_model_index == 0
141 }
142
143 pub fn advance_to_next_model(&self) -> Self {
144 let mut new = self.clone();
145 if let Some(models) = new.models_per_agent.get(new.current_agent_index) {
146 if new.current_model_index + 1 < models.len() {
147 new.current_model_index += 1;
148 } else {
149 new.current_model_index = 0;
150 }
151 }
152 new
153 }
154
155 pub fn switch_to_next_agent(&self) -> Self {
156 let mut new = self.clone();
157 if new.current_agent_index + 1 < new.agents.len() {
158 new.current_agent_index += 1;
159 new.current_model_index = 0;
160 } else {
161 new.current_agent_index = 0;
162 new.current_model_index = 0;
163 new.retry_cycle += 1;
164 }
165 new
166 }
167
168 pub fn reset_for_role(&self, role: AgentRole) -> Self {
169 let mut new = self.clone();
170 new.current_role = role;
171 new.current_agent_index = 0;
172 new.current_model_index = 0;
173 new.retry_cycle = 0;
174 new
175 }
176
177 pub fn reset(&self) -> Self {
178 let mut new = self.clone();
179 new.current_agent_index = 0;
180 new.current_model_index = 0;
181 new
182 }
183
184 pub fn start_retry_cycle(&self) -> Self {
185 let mut new = self.clone();
186 new.current_agent_index = 0;
187 new.current_model_index = 0;
188 new.retry_cycle += 1;
189 new
190 }
191}
192
193#[derive(Clone, Serialize, Deserialize, Debug)]
198pub enum RebaseState {
199 NotStarted,
200 InProgress {
201 original_head: String,
202 target_branch: String,
203 },
204 Conflicted {
205 original_head: String,
206 target_branch: String,
207 files: Vec<PathBuf>,
208 resolution_attempts: u32,
209 },
210 Completed {
211 new_head: String,
212 },
213 Skipped,
214}
215
216impl RebaseState {
217 #[doc(hidden)]
218 #[cfg(any(test, feature = "test-utils"))]
219 pub fn is_terminal(&self) -> bool {
220 matches!(self, RebaseState::Completed { .. } | RebaseState::Skipped)
221 }
222
223 pub fn current_head(&self) -> Option<String> {
224 match self {
225 RebaseState::NotStarted | RebaseState::Skipped => None,
226 RebaseState::InProgress { original_head, .. } => Some(original_head.clone()),
227 RebaseState::Conflicted { .. } => None,
228 RebaseState::Completed { new_head } => Some(new_head.clone()),
229 }
230 }
231
232 #[doc(hidden)]
233 #[cfg(any(test, feature = "test-utils"))]
234 pub fn is_in_progress(&self) -> bool {
235 matches!(
236 self,
237 RebaseState::InProgress { .. } | RebaseState::Conflicted { .. }
238 )
239 }
240}
241
242pub const MAX_VALIDATION_RETRY_ATTEMPTS: u32 = 100;
253
254pub const MAX_DEV_VALIDATION_RETRY_ATTEMPTS: u32 = 2;
260
261#[derive(Clone, Serialize, Deserialize, Debug)]
266pub enum CommitState {
267 NotStarted,
268 Generating { attempt: u32, max_attempts: u32 },
269 Generated { message: String },
270 Committed { hash: String },
271 Skipped,
272}
273
274impl CommitState {
275 #[doc(hidden)]
276 #[cfg(any(test, feature = "test-utils"))]
277 pub fn is_terminal(&self) -> bool {
278 matches!(self, CommitState::Committed { .. } | CommitState::Skipped)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_pipeline_state_initial() {
288 let state = PipelineState::initial(5, 2);
289 assert_eq!(state.phase, PipelinePhase::Planning);
290 assert_eq!(state.total_iterations, 5);
291 assert_eq!(state.total_reviewer_passes, 2);
292 assert!(!state.is_complete());
293 }
294
295 #[test]
296 fn test_agent_chain_initial() {
297 let chain = AgentChainState::initial();
298 assert!(chain.agents.is_empty());
299 assert_eq!(chain.current_agent_index, 0);
300 assert_eq!(chain.retry_cycle, 0);
301 }
302
303 #[test]
304 fn test_agent_chain_with_agents() {
305 let chain = AgentChainState::initial()
306 .with_agents(
307 vec!["claude".to_string(), "codex".to_string()],
308 vec![vec![], vec![]],
309 AgentRole::Developer,
310 )
311 .with_max_cycles(3);
312
313 assert_eq!(chain.agents.len(), 2);
314 assert_eq!(chain.current_agent(), Some(&"claude".to_string()));
315 assert_eq!(chain.max_cycles, 3);
316 }
317
318 #[test]
319 fn test_agent_chain_advance_to_next_model() {
320 let chain = AgentChainState::initial().with_agents(
321 vec!["claude".to_string()],
322 vec![vec!["model1".to_string(), "model2".to_string()]],
323 AgentRole::Developer,
324 );
325
326 let new_chain = chain.advance_to_next_model();
327 assert_eq!(new_chain.current_model_index, 1);
328 assert_eq!(new_chain.current_model(), Some(&"model2".to_string()));
329 }
330
331 #[test]
332 fn test_agent_chain_switch_to_next_agent() {
333 let chain = AgentChainState::initial().with_agents(
334 vec!["claude".to_string(), "codex".to_string()],
335 vec![vec![], vec![]],
336 AgentRole::Developer,
337 );
338
339 let new_chain = chain.switch_to_next_agent();
340 assert_eq!(new_chain.current_agent_index, 1);
341 assert_eq!(new_chain.current_agent(), Some(&"codex".to_string()));
342 assert_eq!(new_chain.retry_cycle, 0);
343 }
344
345 #[test]
346 fn test_agent_chain_exhausted() {
347 let chain = AgentChainState::initial()
348 .with_agents(
349 vec!["claude".to_string()],
350 vec![vec![]],
351 AgentRole::Developer,
352 )
353 .with_max_cycles(3);
354
355 let chain = chain.start_retry_cycle();
356 let chain = chain.start_retry_cycle();
357 let chain = chain.start_retry_cycle();
358
359 assert!(chain.is_exhausted());
360 }
361
362 #[test]
363 fn test_rebase_state_not_started() {
364 let state = RebaseState::NotStarted;
365 assert!(!state.is_terminal());
366 assert!(state.current_head().is_none());
367 assert!(!state.is_in_progress());
368 }
369
370 #[test]
371 fn test_rebase_state_in_progress() {
372 let state = RebaseState::InProgress {
373 original_head: "abc123".to_string(),
374 target_branch: "main".to_string(),
375 };
376 assert!(!state.is_terminal());
377 assert_eq!(state.current_head(), Some("abc123".to_string()));
378 assert!(state.is_in_progress());
379 }
380
381 #[test]
382 fn test_rebase_state_completed() {
383 let state = RebaseState::Completed {
384 new_head: "def456".to_string(),
385 };
386 assert!(state.is_terminal());
387 assert_eq!(state.current_head(), Some("def456".to_string()));
388 assert!(!state.is_in_progress());
389 }
390
391 #[test]
392 fn test_commit_state_not_started() {
393 let state = CommitState::NotStarted;
394 assert!(!state.is_terminal());
395 }
396
397 #[test]
398 fn test_commit_state_generating() {
399 let state = CommitState::Generating {
400 attempt: 1,
401 max_attempts: 3,
402 };
403 assert!(!state.is_terminal());
404 }
405
406 #[test]
407 fn test_commit_state_committed() {
408 let state = CommitState::Committed {
409 hash: "abc123".to_string(),
410 };
411 assert!(state.is_terminal());
412 }
413
414 #[test]
415 fn test_is_complete_during_finalizing() {
416 let state = PipelineState {
419 phase: PipelinePhase::Finalizing,
420 ..PipelineState::initial(5, 2)
421 };
422 assert!(
423 !state.is_complete(),
424 "Finalizing phase should not be complete - event loop must continue"
425 );
426 }
427
428 #[test]
429 fn test_is_complete_after_finalization() {
430 let state = PipelineState {
432 phase: PipelinePhase::Complete,
433 ..PipelineState::initial(5, 2)
434 };
435 assert!(state.is_complete(), "Complete phase should be complete");
436 }
437}