1use crate::agents::AgentRole;
11use crate::checkpoint::{save_checkpoint, PipelineCheckpoint, PipelinePhase};
12use crate::files::{delete_plan_file, update_status};
13use crate::files::{extract_plan, extract_plan_from_logs_text};
14use crate::git_helpers::{git_snapshot, CommitResultFallback};
15use crate::logger::print_progress;
16use crate::phases::commit::commit_with_generated_message;
17use crate::phases::get_primary_commit_agent;
18use crate::phases::integrity::ensure_prompt_integrity;
19use crate::pipeline::{run_with_fallback, PipelineRuntime};
20use crate::prompts::{prompt_for_agent, Action, ContextLevel, PromptConfig, Role};
21use std::fs;
22use std::path::Path;
23use std::process::Command;
24
25use super::context::PhaseContext;
26
27pub struct DevelopmentResult {
29 pub had_errors: bool,
31}
32
33pub fn run_development_phase(
50 ctx: &mut PhaseContext<'_>,
51 start_iter: u32,
52 resuming_from_development: bool,
53) -> anyhow::Result<DevelopmentResult> {
54 let mut had_errors = false;
55 let mut prev_snap = git_snapshot()?;
56 let developer_context = ContextLevel::from(ctx.config.developer_context);
57
58 for i in start_iter..=ctx.config.developer_iters {
59 ctx.logger.subheader(&format!(
60 "Iteration {} of {}",
61 i, ctx.config.developer_iters
62 ));
63 print_progress(i, ctx.config.developer_iters, "Overall");
64
65 let resuming_into_development = resuming_from_development && i == start_iter;
66
67 if resuming_into_development {
69 ctx.logger
70 .info("Resuming at development step; skipping plan generation");
71 } else {
72 run_planning_step(ctx, i)?;
73 }
74
75 let plan_ok = verify_plan_exists(ctx, i, resuming_into_development)?;
77 if !plan_ok {
78 anyhow::bail!("Planning phase did not create a non-empty .agent/PLAN.md");
79 }
80 ctx.logger.success("PLAN.md created");
81
82 if ctx.config.features.checkpoint_enabled {
84 let _ = save_checkpoint(&PipelineCheckpoint::new(
85 PipelinePhase::Development,
86 i,
87 ctx.config.developer_iters,
88 0,
89 ctx.config.reviewer_reviews,
90 ctx.developer_agent,
91 ctx.reviewer_agent,
92 ));
93 }
94
95 ctx.logger.info("Executing plan...");
97 update_status("Starting development iteration", ctx.config.isolation_mode)?;
98
99 let prompt_md = fs::read_to_string("PROMPT.md").unwrap_or_default();
103 let plan_md = fs::read_to_string(".agent/PLAN.md").unwrap_or_default();
104
105 let prompt = prompt_for_agent(
106 Role::Developer,
107 Action::Iterate,
108 developer_context,
109 ctx.template_context,
110 PromptConfig::new()
111 .with_iterations(i, ctx.config.developer_iters)
112 .with_prompt_and_plan(prompt_md, plan_md),
113 );
114
115 let mut runtime = PipelineRuntime {
116 timer: ctx.timer,
117 logger: ctx.logger,
118 colors: ctx.colors,
119 config: ctx.config,
120 #[cfg(any(test, feature = "test-utils"))]
121 agent_executor: None,
122 };
123 let exit_code = run_with_fallback(
124 AgentRole::Developer,
125 &format!("run #{i}"),
126 &prompt,
127 &format!(".agent/logs/developer_{i}"),
128 &mut runtime,
129 ctx.registry,
130 ctx.developer_agent,
131 )?;
132
133 if exit_code != 0 {
134 ctx.logger.error(&format!(
135 "Iteration {i} encountered an error but continuing"
136 ));
137 had_errors = true;
138 }
139
140 ctx.stats.developer_runs_completed += 1;
141 update_status("Completed progress step", ctx.config.isolation_mode)?;
142
143 let snap = git_snapshot()?;
144 if snap == prev_snap {
145 if snap.is_empty() {
146 ctx.logger
147 .warn("No git-status change detected (repository is clean)");
148 } else {
149 ctx.logger.warn(&format!(
150 "No git-status change detected (existing changes: {})",
151 snap.lines().count()
152 ));
153 }
154 } else {
155 ctx.logger.success(&format!(
156 "Repository modified ({} file(s) changed)",
157 snap.lines().count()
158 ));
159 ctx.stats.changes_detected += 1;
160 handle_commit_after_development(ctx)?;
161 }
162 prev_snap = snap;
163
164 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
166 run_fast_check(ctx, fast_cmd, i)?;
167 }
168
169 ensure_prompt_integrity(ctx.logger, "development", i);
172
173 ctx.logger.info("Deleting PLAN.md...");
175 if let Err(err) = delete_plan_file() {
176 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
177 }
178 ctx.logger.success("PLAN.md deleted");
179 }
180
181 Ok(DevelopmentResult { had_errors })
182}
183
184fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
189 if ctx.config.features.checkpoint_enabled {
191 let _ = save_checkpoint(&PipelineCheckpoint::new(
192 PipelinePhase::Planning,
193 iteration,
194 ctx.config.developer_iters,
195 0,
196 ctx.config.reviewer_reviews,
197 ctx.developer_agent,
198 ctx.reviewer_agent,
199 ));
200 }
201
202 ctx.logger.info("Creating plan from PROMPT.md...");
203 update_status("Starting planning phase", ctx.config.isolation_mode)?;
204
205 let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
209
210 let plan_prompt = prompt_for_agent(
211 Role::Developer,
212 Action::Plan,
213 ContextLevel::Normal,
214 ctx.template_context,
215 prompt_md_content
216 .map(|content| PromptConfig::new().with_prompt_md(content))
217 .unwrap_or_default(),
218 );
219
220 let log_dir = format!(".agent/logs/planning_{iteration}");
221 let mut runtime = PipelineRuntime {
222 timer: ctx.timer,
223 logger: ctx.logger,
224 colors: ctx.colors,
225 config: ctx.config,
226 #[cfg(any(test, feature = "test-utils"))]
227 agent_executor: None,
228 };
229 let _exit_code = run_with_fallback(
230 AgentRole::Developer,
231 &format!("planning #{iteration}"),
232 &plan_prompt,
233 &log_dir,
234 &mut runtime,
235 ctx.registry,
236 ctx.developer_agent,
237 )?;
238
239 let plan_path = Path::new(".agent/PLAN.md");
243 let log_dir_path = Path::new(&log_dir);
244
245 if let Some(parent) = plan_path.parent() {
247 fs::create_dir_all(parent)?;
248 }
249
250 let extraction = extract_plan(log_dir_path)?;
251
252 if let Some(content) = extraction.raw_content {
253 fs::write(plan_path, &content)?;
255
256 if extraction.is_valid {
257 ctx.logger
258 .success("Plan extracted from agent output (JSON)");
259 } else {
260 ctx.logger.warn(&format!(
261 "Plan written but validation failed: {}",
262 extraction.validation_warning.unwrap_or_default()
263 ));
264 }
265 } else {
266 ctx.logger
268 .info("No JSON result event found, trying text-based extraction...");
269
270 if let Some(text_plan) = extract_plan_from_logs_text(log_dir_path)? {
271 fs::write(plan_path, &text_plan)?;
272 ctx.logger
273 .success("Plan extracted from agent output (text fallback)");
274 } else {
275 let agent_wrote_file = plan_path
277 .exists()
278 .then(|| fs::read_to_string(plan_path).ok())
279 .flatten()
280 .is_some_and(|s| !s.trim().is_empty());
281
282 if agent_wrote_file {
283 ctx.logger.info("Using agent-written PLAN.md (legacy mode)");
284 } else {
285 let placeholder = "# Plan\n\nAgent produced no extractable plan content.\n";
289 fs::write(plan_path, placeholder)?;
290 ctx.logger
291 .error("No plan content found in agent output - wrote placeholder");
292 anyhow::bail!(
293 "Planning agent completed successfully but no plan was found in output"
294 );
295 }
296 }
297 }
298
299 Ok(())
300}
301
302fn verify_plan_exists(
309 ctx: &mut PhaseContext<'_>,
310 iteration: u32,
311 resuming_into_development: bool,
312) -> anyhow::Result<bool> {
313 let plan_path = Path::new(".agent/PLAN.md");
314
315 let plan_ok = plan_path
316 .exists()
317 .then(|| fs::read_to_string(plan_path).ok())
318 .flatten()
319 .is_some_and(|s| !s.trim().is_empty());
320
321 if !plan_ok && resuming_into_development {
323 ctx.logger
324 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
325 run_planning_step(ctx, iteration)?;
326
327 let plan_ok = plan_path
329 .exists()
330 .then(|| fs::read_to_string(plan_path).ok())
331 .flatten()
332 .is_some_and(|s| !s.trim().is_empty());
333
334 return Ok(plan_ok);
335 }
336
337 Ok(plan_ok)
338}
339
340fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
342 let argv = crate::common::split_command(fast_cmd)
343 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
344 if argv.is_empty() {
345 ctx.logger
346 .warn("FAST_CHECK_CMD is empty; skipping fast check");
347 return Ok(());
348 }
349
350 let display_cmd = crate::common::format_argv_for_log(&argv);
351 ctx.logger.info(&format!(
352 "Running fast check: {}{}{}",
353 ctx.colors.dim(),
354 display_cmd,
355 ctx.colors.reset()
356 ));
357
358 let Some((program, cmd_args)) = argv.split_first() else {
359 ctx.logger
360 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
361 return Ok(());
362 };
363 let status = Command::new(program).args(cmd_args).status()?;
364
365 if status.success() {
366 ctx.logger.success("Fast check passed");
367 } else {
368 ctx.logger.warn("Fast check had issues (non-blocking)");
369 }
370
371 Ok(())
372}
373
374fn handle_commit_after_development(ctx: &mut PhaseContext<'_>) -> anyhow::Result<()> {
380 let commit_agent = get_primary_commit_agent(ctx);
382
383 if let Some(agent) = commit_agent {
384 ctx.logger.info(&format!(
385 "Creating commit with auto-generated message (agent: {agent})..."
386 ));
387
388 let diff = match crate::git_helpers::git_diff() {
390 Ok(d) => d,
391 Err(e) => {
392 ctx.logger
393 .error(&format!("Failed to get diff for commit: {e}"));
394 return Err(anyhow::anyhow!(e));
395 }
396 };
397
398 let git_name = ctx.config.git_user_name.as_deref();
400 let git_email = ctx.config.git_user_email.as_deref();
401
402 let result = commit_with_generated_message(&diff, &agent, git_name, git_email, ctx);
403
404 match result {
405 CommitResultFallback::Success(oid) => {
406 ctx.logger
407 .success(&format!("Commit created successfully: {oid}"));
408 ctx.stats.commits_created += 1;
409 }
410 CommitResultFallback::NoChanges => {
411 ctx.logger.info("No commit created (no meaningful changes)");
413 }
414 CommitResultFallback::Failed(err) => {
415 ctx.logger.error(&format!(
417 "Failed to create commit (git operation failed): {err}"
418 ));
419 return Err(anyhow::anyhow!(err));
421 }
422 }
423 } else {
424 ctx.logger
425 .warn("Unable to get primary commit agent for commit");
426 }
427
428 Ok(())
429}