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 exit_code = {
116 let mut runtime = PipelineRuntime {
117 timer: ctx.timer,
118 logger: ctx.logger,
119 colors: ctx.colors,
120 config: ctx.config,
121 };
122 run_with_fallback(
123 AgentRole::Developer,
124 &format!("run #{i}"),
125 &prompt,
126 &format!(".agent/logs/developer_{i}"),
127 &mut runtime,
128 ctx.registry,
129 ctx.developer_agent,
130 )?
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 ctx.logger.warn("No git-status change detected");
146 } else {
147 ctx.logger.success("Repository modified");
148 ctx.stats.changes_detected += 1;
149 handle_commit_after_development(ctx)?;
150 }
151 prev_snap = snap;
152
153 if let Some(ref fast_cmd) = ctx.config.fast_check_cmd {
155 run_fast_check(ctx, fast_cmd, i)?;
156 }
157
158 ensure_prompt_integrity(ctx.logger, "development", i);
161
162 ctx.logger.info("Deleting PLAN.md...");
164 if let Err(err) = delete_plan_file() {
165 ctx.logger.warn(&format!("Failed to delete PLAN.md: {err}"));
166 }
167 ctx.logger.success("PLAN.md deleted");
168 }
169
170 Ok(DevelopmentResult { had_errors })
171}
172
173fn run_planning_step(ctx: &mut PhaseContext<'_>, iteration: u32) -> anyhow::Result<()> {
178 if ctx.config.features.checkpoint_enabled {
180 let _ = save_checkpoint(&PipelineCheckpoint::new(
181 PipelinePhase::Planning,
182 iteration,
183 ctx.config.developer_iters,
184 0,
185 ctx.config.reviewer_reviews,
186 ctx.developer_agent,
187 ctx.reviewer_agent,
188 ));
189 }
190
191 ctx.logger.info("Creating plan from PROMPT.md...");
192 update_status("Starting planning phase", ctx.config.isolation_mode)?;
193
194 let prompt_md_content = std::fs::read_to_string("PROMPT.md").ok();
198
199 let plan_prompt = prompt_for_agent(
200 Role::Developer,
201 Action::Plan,
202 ContextLevel::Normal,
203 ctx.template_context,
204 prompt_md_content
205 .map(|content| PromptConfig::new().with_prompt_md(content))
206 .unwrap_or_default(),
207 );
208
209 let log_dir = format!(".agent/logs/planning_{iteration}");
210 let _exit_code = {
211 let mut runtime = PipelineRuntime {
212 timer: ctx.timer,
213 logger: ctx.logger,
214 colors: ctx.colors,
215 config: ctx.config,
216 };
217 run_with_fallback(
218 AgentRole::Developer,
219 &format!("planning #{iteration}"),
220 &plan_prompt,
221 &log_dir,
222 &mut runtime,
223 ctx.registry,
224 ctx.developer_agent,
225 )
226 }?;
227
228 let plan_path = Path::new(".agent/PLAN.md");
232 let log_dir_path = Path::new(&log_dir);
233
234 if let Some(parent) = plan_path.parent() {
236 fs::create_dir_all(parent)?;
237 }
238
239 let extraction = extract_plan(log_dir_path)?;
240
241 if let Some(content) = extraction.raw_content {
242 fs::write(plan_path, &content)?;
244
245 if extraction.is_valid {
246 ctx.logger
247 .success("Plan extracted from agent output (JSON)");
248 } else {
249 ctx.logger.warn(&format!(
250 "Plan written but validation failed: {}",
251 extraction.validation_warning.unwrap_or_default()
252 ));
253 }
254 } else {
255 ctx.logger
257 .info("No JSON result event found, trying text-based extraction...");
258
259 if let Some(text_plan) = extract_plan_from_logs_text(log_dir_path)? {
260 fs::write(plan_path, &text_plan)?;
261 ctx.logger
262 .success("Plan extracted from agent output (text fallback)");
263 } else {
264 let agent_wrote_file = plan_path
266 .exists()
267 .then(|| fs::read_to_string(plan_path).ok())
268 .flatten()
269 .is_some_and(|s| !s.trim().is_empty());
270
271 if agent_wrote_file {
272 ctx.logger.info("Using agent-written PLAN.md (legacy mode)");
273 } else {
274 let placeholder = "# Plan\n\nAgent produced no extractable plan content.\n";
278 fs::write(plan_path, placeholder)?;
279 ctx.logger
280 .error("No plan content found in agent output - wrote placeholder");
281 anyhow::bail!(
282 "Planning agent completed successfully but no plan was found in output"
283 );
284 }
285 }
286 }
287
288 Ok(())
289}
290
291fn verify_plan_exists(
298 ctx: &mut PhaseContext<'_>,
299 iteration: u32,
300 resuming_into_development: bool,
301) -> anyhow::Result<bool> {
302 let plan_path = Path::new(".agent/PLAN.md");
303
304 let plan_ok = plan_path
305 .exists()
306 .then(|| fs::read_to_string(plan_path).ok())
307 .flatten()
308 .is_some_and(|s| !s.trim().is_empty());
309
310 if !plan_ok && resuming_into_development {
312 ctx.logger
313 .warn("Missing .agent/PLAN.md; rerunning plan generation to recover");
314 run_planning_step(ctx, iteration)?;
315
316 let plan_ok = plan_path
318 .exists()
319 .then(|| fs::read_to_string(plan_path).ok())
320 .flatten()
321 .is_some_and(|s| !s.trim().is_empty());
322
323 return Ok(plan_ok);
324 }
325
326 Ok(plan_ok)
327}
328
329fn run_fast_check(ctx: &PhaseContext<'_>, fast_cmd: &str, iteration: u32) -> anyhow::Result<()> {
331 let argv = crate::common::split_command(fast_cmd)
332 .map_err(|e| anyhow::anyhow!("FAST_CHECK_CMD parse error (iteration {iteration}): {e}"))?;
333 if argv.is_empty() {
334 ctx.logger
335 .warn("FAST_CHECK_CMD is empty; skipping fast check");
336 return Ok(());
337 }
338
339 let display_cmd = crate::common::format_argv_for_log(&argv);
340 ctx.logger.info(&format!(
341 "Running fast check: {}{}{}",
342 ctx.colors.dim(),
343 display_cmd,
344 ctx.colors.reset()
345 ));
346
347 let Some((program, cmd_args)) = argv.split_first() else {
348 ctx.logger
349 .warn("FAST_CHECK_CMD is empty after parsing; skipping fast check");
350 return Ok(());
351 };
352 let status = Command::new(program).args(cmd_args).status()?;
353
354 if status.success() {
355 ctx.logger.success("Fast check passed");
356 } else {
357 ctx.logger.warn("Fast check had issues (non-blocking)");
358 }
359
360 Ok(())
361}
362
363fn handle_commit_after_development(ctx: &mut PhaseContext<'_>) -> anyhow::Result<()> {
369 let commit_agent = get_primary_commit_agent(ctx);
371
372 if let Some(agent) = commit_agent {
373 ctx.logger.info(&format!(
374 "Creating commit with auto-generated message (agent: {agent})..."
375 ));
376
377 let diff = match crate::git_helpers::git_diff() {
379 Ok(d) => d,
380 Err(e) => {
381 ctx.logger
382 .error(&format!("Failed to get diff for commit: {e}"));
383 return Err(anyhow::anyhow!(e));
384 }
385 };
386
387 let git_name = ctx.config.git_user_name.as_deref();
389 let git_email = ctx.config.git_user_email.as_deref();
390
391 match commit_with_generated_message(&diff, &agent, git_name, git_email, ctx) {
392 CommitResultFallback::Success(oid) => {
393 ctx.logger
394 .success(&format!("Commit created successfully: {oid}"));
395 ctx.stats.commits_created += 1;
396 }
397 CommitResultFallback::NoChanges => {
398 ctx.logger.info("No commit created (no meaningful changes)");
400 }
401 CommitResultFallback::Failed(err) => {
402 ctx.logger.error(&format!(
404 "Failed to create commit (git operation failed): {err}"
405 ));
406 return Err(anyhow::anyhow!(err));
408 }
409 }
410 } else {
411 ctx.logger
412 .warn("Unable to get primary commit agent for commit");
413 }
414
415 Ok(())
416}