1use crate::agents::AgentRole;
11use crate::checkpoint::restore::ResumeContext;
12use crate::checkpoint::{save_checkpoint, CheckpointBuilder, PipelinePhase};
13use crate::files::{delete_plan_file, update_status};
14use crate::files::{extract_plan, extract_plan_from_logs_text};
15use crate::git_helpers::{git_snapshot, CommitResultFallback};
16use crate::logger::print_progress;
17use crate::phases::commit::commit_with_generated_message;
18use crate::phases::get_primary_commit_agent;
19use crate::phases::integrity::ensure_prompt_integrity;
20use crate::pipeline::{run_with_fallback, PipelineRuntime};
21use crate::prompts::{
22 get_stored_or_generate_prompt, prompt_for_agent, Action, ContextLevel, PromptConfig, Role,
23};
24use std::fs;
25use std::path::Path;
26use std::process::Command;
27
28use super::context::PhaseContext;
29
30use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
31
32use std::time::Instant;
33
34pub struct DevelopmentResult {
36 pub had_errors: bool,
38}
39
40pub fn run_development_phase(
57 ctx: &mut PhaseContext<'_>,
58 start_iter: u32,
59 resume_context: Option<&ResumeContext>,
60) -> anyhow::Result<DevelopmentResult> {
61 let mut had_errors = false;
62 let mut prev_snap = git_snapshot()?;
63 let developer_context = ContextLevel::from(ctx.config.developer_context);
64
65 for i in start_iter..=ctx.config.developer_iters {
66 ctx.logger.subheader(&format!(
67 "Iteration {} of {}",
68 i, ctx.config.developer_iters
69 ));
70 print_progress(i, ctx.config.developer_iters, "Overall");
71
72 let resuming_into_development = resume_context.is_some() && i == start_iter;
73
74 if resuming_into_development {
76 ctx.logger
77 .info("Resuming at development step; skipping plan generation");
78 } else {
79 run_planning_step(ctx, i)?;
80 }
81
82 let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
84 if !plan_ok {
85 anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
86 }
87 ctx.logger.success("PLAN.md created");
88
89 if ctx.config.features.checkpoint_enabled {
91 let builder = CheckpointBuilder::new()
92 .phase(PipelinePhase::Development, i, ctx.config.developer_iters)
93 .reviewer_pass(0, ctx.config.reviewer_reviews)
94 .capture_from_context(
95 ctx.config,
96 ctx.registry,
97 ctx.developer_agent,
98 ctx.reviewer_agent,
99 ctx.logger,
100 &ctx.run_context,
101 )
102 .with_execution_history(ctx.execution_history.clone())
103 .with_prompt_history(ctx.clone_prompt_history());
104
105 if let Some(checkpoint) = builder.build() {
106 let _ = save_checkpoint(&checkpoint);
107 }
108 }
109
110 ctx.record_developer_iteration();
112
113 ctx.logger.info("Executing plan...");
115 update_status("Starting development iteration", ctx.config.isolation_mode)?;
116
117 let prompt_md = fs::read_to_string("PROMPT.md").unwrap_or_default();
121 let plan_md = fs::read_to_string(".agent/PLAN.md").unwrap_or_default();
122
123 let mut prompt_config = PromptConfig::new()
124 .with_iterations(i, ctx.config.developer_iters)
125 .with_prompt_and_plan(prompt_md, plan_md);
126
127 if resuming_into_development {
129 if let Some(resume_ctx) = resume_context {
130 prompt_config = prompt_config.with_resume_context(resume_ctx.clone());
131 }
132 }
133
134 let prompt_key = format!("development_{}", i);
136 let (prompt, was_replayed) =
137 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
138 prompt_for_agent(
139 Role::Developer,
140 Action::Iterate,
141 developer_context,
142 ctx.template_context,
143 prompt_config.clone(),
144 )
145 });
146
147 if !was_replayed {
149 ctx.capture_prompt(&prompt_key, &prompt);
150 } else {
151 ctx.logger.info(&format!(
152 "Using stored prompt from checkpoint for determinism: {}",
153 prompt_key
154 ));
155 }
156
157 let dev_start_time = Instant::now();
158
159 let exit_code = {
160 let mut runtime = PipelineRuntime {
161 timer: ctx.timer,
162 logger: ctx.logger,
163 colors: ctx.colors,
164 config: ctx.config,
165 #[cfg(any(test, feature = "test-utils"))]
166 agent_executor: None,
167 };
168 run_with_fallback(
169 AgentRole::Developer,
170 &format!("run #{i}"),
171 &prompt,
172 &format!(".agent/logs/developer_{i}"),
173 &mut runtime,
174 ctx.registry,
175 ctx.developer_agent,
176 )?
177 };
178
179 if exit_code != 0 {
180 ctx.logger.error(&format!(
181 "Iteration {i} encountered an error but continuing"
182 ));
183 had_errors = true;
184 }
185
186 ctx.stats.developer_runs_completed += 1;
187
188 {
189 let duration = dev_start_time.elapsed().as_secs();
190 let outcome = if exit_code != 0 {
191 StepOutcome::failure(format!("Agent exited with code {exit_code}"), true)
192 } else {
193 StepOutcome::success(None, vec![])
194 };
195 let step = ExecutionStep::new("Development", i, "dev_run", outcome)
196 .with_agent(ctx.developer_agent)
197 .with_duration(duration);
198 ctx.execution_history.add_step(step);
199 }
200 update_status("Completed progress step", ctx.config.isolation_mode)?;
201
202 let snap = git_snapshot()?;
203 if snap == prev_snap {
204 if snap.is_empty() {
205 ctx.logger
206 .warn("No git-status change detected (repository is clean)");
207 } else {
208 ctx.logger.warn(&format!(
209 "No git-status change detected (existing changes: {})",
210 snap.lines().count()
211 ));
212 }
213 } else {
214 ctx.logger.success(&format!(
215 "Repository modified ({} file(s) changed)",
216 snap.lines().count()
217 ));
218 ctx.stats.changes_detected += 1;
219 handle_commit_after_development(ctx, i)?;
220 }
221 prev_snap = snap;
222
223 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
225 run_fast_check(ctx, fast_cmd, i)?;
226 }
227
228 ensure_prompt_integrity(ctx.logger, "development", i);
231
232 ctx.logger.info("Deleting PLAN.md...");
234 if let Err(err) = delete_plan_file() {
235 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
236 }
237 ctx.logger.success("PLAN.md deleted");
238
239 if ctx.config.features.checkpoint_enabled {
242 let next_iteration = i + 1;
243 let builder = CheckpointBuilder::new()
244 .phase(
245 PipelinePhase::Development,
246 next_iteration,
247 ctx.config.developer_iters,
248 )
249 .reviewer_pass(0, ctx.config.reviewer_reviews)
250 .capture_from_context(
251 ctx.config,
252 ctx.registry,
253 ctx.developer_agent,
254 ctx.reviewer_agent,
255 ctx.logger,
256 &ctx.run_context,
257 )
258 .with_execution_history(ctx.execution_history.clone())
259 .with_prompt_history(ctx.clone_prompt_history());
260
261 if let Some(checkpoint) = builder.build() {
262 let _ = save_checkpoint(&checkpoint);
263 }
264 }
265 }
266
267 Ok(DevelopmentResult { had_errors })
268}
269
270fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
275 let start_time = Instant::now();
276 if ctx.config.features.checkpoint_enabled {
278 let builder = CheckpointBuilder::new()
279 .phase(
280 PipelinePhase::Planning,
281 iteration,
282 ctx.config.developer_iters,
283 )
284 .reviewer_pass(0, ctx.config.reviewer_reviews)
285 .capture_from_context(
286 ctx.config,
287 ctx.registry,
288 ctx.developer_agent,
289 ctx.reviewer_agent,
290 ctx.logger,
291 &ctx.run_context,
292 )
293 .with_execution_history(ctx.execution_history.clone())
294 .with_prompt_history(ctx.clone_prompt_history());
295
296 if let Some(checkpoint) = builder.build() {
297 let _ = save_checkpoint(&checkpoint);
298 }
299 }
300
301 ctx.logger.info("Creating plan from PROMPT.md...");
302 update_status("Starting planning phase", ctx.config.isolation_mode)?;
303
304 let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
308
309 let prompt_key = format!("planning_{}", iteration);
312 let (plan_prompt, was_replayed) =
313 get_stored_or_generate_prompt(&prompt_key, &ctx.prompt_history, || {
314 prompt_for_agent(
315 Role::Developer,
316 Action::Plan,
317 ContextLevel::Normal,
318 ctx.template_context,
319 prompt_md_content
320 .as_ref()
321 .map(|content| PromptConfig::new().with_prompt_md(content.clone()))
322 .unwrap_or_default(),
323 )
324 });
325
326 if !was_replayed {
328 ctx.capture_prompt(&prompt_key, &plan_prompt);
329 } else {
330 ctx.logger.info(&format!(
331 "Using stored prompt from checkpoint for determinism: {}",
332 prompt_key
333 ));
334 }
335
336 let log_dir = format!(".agent/logs/planning_{iteration}");
337 let mut runtime = PipelineRuntime {
338 timer: ctx.timer,
339 logger: ctx.logger,
340 colors: ctx.colors,
341 config: ctx.config,
342 #[cfg(any(test, feature = "test-utils"))]
343 agent_executor: None,
344 };
345 let _exit_code = run_with_fallback(
346 AgentRole::Developer,
347 &format!("planning #{iteration}"),
348 &plan_prompt,
349 &log_dir,
350 &mut runtime,
351 ctx.registry,
352 ctx.developer_agent,
353 )?;
354
355 let plan_path = Path::new(".agent/PLAN.md");
359 let log_dir_path = Path::new(&log_dir);
360
361 if let Some(parent) = plan_path.parent() {
363 fs::create_dir_all(parent)?;
364 }
365
366 let extraction = extract_plan(log_dir_path)?;
367
368 if let Some(content) = extraction.raw_content {
369 fs::write(plan_path, &content)?;
371
372 if extraction.is_valid {
373 ctx.logger
374 .success("Plan extracted from agent output (JSON)");
375 } else {
376 ctx.logger.warn(&format!(
377 "Plan written but validation failed: {}",
378 extraction.validation_warning.unwrap_or_default()
379 ));
380 }
381 } else {
382 ctx.logger
384 .info("No JSON result event found, trying text-based extraction...");
385
386 if let Some(text_plan) = extract_plan_from_logs_text(log_dir_path)? {
387 fs::write(plan_path, &text_plan)?;
388 ctx.logger
389 .success("Plan extracted from agent output (text fallback)");
390 } else {
391 let agent_wrote_file = plan_path
393 .exists()
394 .then(|| fs::read_to_string(plan_path).ok())
395 .flatten()
396 .is_some_and(|s| !s.trim().is_empty());
397
398 if agent_wrote_file {
399 ctx.logger.info("Using agent-written PLAN.md (legacy mode)");
400 } else {
401 let placeholder = "# Plan\n\nAgent produced no extractable plan content.\n";
405 fs::write(plan_path, placeholder)?;
406 ctx.logger
407 .error("No plan content found in agent output - wrote placeholder");
408 anyhow::bail!(
409 "Planning agent completed successfully but no plan was found in output"
410 );
411 }
412 }
413 }
414
415 {
416 let duration = start_time.elapsed().as_secs();
417 let step = ExecutionStep::new(
418 "Planning",
419 iteration,
420 "plan_generation",
421 StepOutcome::success(None, vec![".agent/PLAN.md".to_string()]),
422 )
423 .with_agent(ctx.developer_agent)
424 .with_duration(duration);
425 ctx.execution_history.add_step(step);
426 }
427
428 Ok(())
429}
430
431fn verify_plan_exists(
438 ctx: &mut PhaseContext<'_>,
439 iteration: u32,
440 resuming_into_development: bool,
441) -> anyhow::Result<bool> {
442 let plan_path = Path::new(".agent/PLAN.md");
443
444 let plan_ok = plan_path
445 .exists()
446 .then(|| fs::read_to_string(plan_path).ok())
447 .flatten()
448 .is_some_and(|s| !s.trim().is_empty());
449
450 if !plan_ok && resuming_into_development {
452 ctx.logger
453 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
454 run_planning_step(ctx, iteration)?;
455
456 let plan_ok = plan_path
458 .exists()
459 .then(|| fs::read_to_string(plan_path).ok())
460 .flatten()
461 .is_some_and(|s| !s.trim().is_empty());
462
463 return Ok(plan_ok);
464 }
465
466 Ok(plan_ok)
467}
468
469fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
471 let argv = crate::common::split_command(fast_cmd)
472 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
473 if argv.is_empty() {
474 ctx.logger
475 .warn("FAST_CHECK_CMD is empty; skipping fast check");
476 return Ok(());
477 }
478
479 let display_cmd = crate::common::format_argv_for_log(&argv);
480 ctx.logger.info(&format!(
481 "Running fast check: {}{}{}",
482 ctx.colors.dim(),
483 display_cmd,
484 ctx.colors.reset()
485 ));
486
487 let Some((program, cmd_args)) = argv.split_first() else {
488 ctx.logger
489 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
490 return Ok(());
491 };
492 let status = Command::new(program).args(cmd_args).status()?;
493
494 if status.success() {
495 ctx.logger.success("Fast check passed");
496 } else {
497 ctx.logger.warn("Fast check had issues (non-blocking)");
498 }
499
500 Ok(())
501}
502
503fn handle_commit_after_development(
509 ctx: &mut PhaseContext<'_>,
510 iteration: u32,
511) -> anyhow::Result<()> {
512 let start_time = Instant::now();
513 let commit_agent = get_primary_commit_agent(ctx);
515
516 if let Some(agent) = commit_agent {
517 ctx.logger.info(&format!(
518 "Creating commit with auto-generated message (agent: {agent})..."
519 ));
520
521 let diff = match crate::git_helpers::git_diff() {
523 Ok(d) => d,
524 Err(e) => {
525 ctx.logger
526 .error(&format!("Failed to get diff for commit: {e}"));
527 return Err(anyhow::anyhow!(e));
528 }
529 };
530
531 let git_name = ctx.config.git_user_name.as_deref();
533 let git_email = ctx.config.git_user_email.as_deref();
534
535 let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
536
537 match result {
538 CommitResultFallback::Success(oid) => {
539 ctx.logger
540 .success(&format!("Commit created successfully: {oid}"));
541 ctx.stats.commits_created += 1;
542
543 {
544 let duration = start_time.elapsed().as_secs();
545 let step = ExecutionStep::new(
546 "Development",
547 iteration,
548 "commit",
549 StepOutcome::success(Some(oid.to_string()), vec![]),
550 )
551 .with_agent(&agent)
552 .with_duration(duration);
553 ctx.execution_history.add_step(step);
554 }
555 }
556 CommitResultFallback::NoChanges => {
557 ctx.logger.info("No commit created (no meaningful changes)");
559
560 {
561 let duration = start_time.elapsed().as_secs();
562 let step = ExecutionStep::new(
563 "Development",
564 iteration,
565 "commit",
566 StepOutcome::skipped("No meaningful changes to commit".to_string()),
567 )
568 .with_duration(duration);
569 ctx.execution_history.add_step(step);
570 }
571 }
572 CommitResultFallback::Failed(err) => {
573 ctx.logger.error(&format!(
575 "Failed to create commit (git operation failed): {err}"
576 ));
577
578 {
579 let duration = start_time.elapsed().as_secs();
580 let step = ExecutionStep::new(
581 "Development",
582 iteration,
583 "commit",
584 StepOutcome::failure(err.to_string(), false),
585 )
586 .with_duration(duration);
587 ctx.execution_history.add_step(step);
588 }
589
590 return Err(anyhow::anyhow!(err));
592 }
593 }
594 } else {
595 ctx.logger
596 .warn("Unable to get primary commit agent for commit");
597
598 {
599 let duration = start_time.elapsed().as_secs();
600 let step = ExecutionStep::new(
601 "Development",
602 iteration,
603 "commit",
604 StepOutcome::failure("No commit agent available".to_string(), true),
605 )
606 .with_duration(duration);
607 ctx.execution_history.add_step(step);
608 }
609 }
610
611 Ok(())
612}