@@ -265,6 +265,14 @@ class ResponseRepairer: |
| 265 | 265 | retry_number: int, |
| 266 | 266 | max_empty_retries: int, |
| 267 | 267 | ) -> str: |
| 268 | + if dod is not None: |
| 269 | + minimal_retry_message = self._build_early_concrete_write_retry_message( |
| 270 | + dod, |
| 271 | + retry_number=retry_number, |
| 272 | + max_empty_retries=max_empty_retries, |
| 273 | + ) |
| 274 | + if minimal_retry_message is not None: |
| 275 | + return minimal_retry_message |
| 268 | 276 | if dod is not None and self._should_compact_empty_retry_message(dod): |
| 269 | 277 | compact_lines: list[str] = [] |
| 270 | 278 | compact_lines.extend(self._compact_planned_artifact_lines(dod)) |
@@ -368,6 +376,81 @@ class ResponseRepairer: |
| 368 | 376 | ] |
| 369 | 377 | ) |
| 370 | 378 | |
| 379 | + def _build_early_concrete_write_retry_message( |
| 380 | + self, |
| 381 | + dod: DefinitionOfDone, |
| 382 | + *, |
| 383 | + retry_number: int, |
| 384 | + max_empty_retries: int, |
| 385 | + ) -> str | None: |
| 386 | + if retry_number < 3: |
| 387 | + return None |
| 388 | + if not self._has_confirmed_output_file_progress(dod): |
| 389 | + return None |
| 390 | + if self._has_confirmed_substantive_output_file_progress(dod): |
| 391 | + return None |
| 392 | + |
| 393 | + next_missing_artifact = self._preferred_resume_missing_artifact(dod) |
| 394 | + next_pending = self._preferred_resume_pending_item( |
| 395 | + dod, |
| 396 | + missing_artifact=next_missing_artifact, |
| 397 | + ) |
| 398 | + inferred_pending_target = ( |
| 399 | + self._infer_pending_item_output_target(dod, next_pending) |
| 400 | + if next_pending |
| 401 | + else None |
| 402 | + ) |
| 403 | + concrete_target: Path | None = None |
| 404 | + if inferred_pending_target is not None and not inferred_pending_target.exists(): |
| 405 | + concrete_target = inferred_pending_target.expanduser().resolve(strict=False) |
| 406 | + elif next_missing_artifact is not None and not next_missing_artifact[1]: |
| 407 | + concrete_target = next_missing_artifact[0].expanduser().resolve(strict=False) |
| 408 | + if concrete_target is None or not concrete_target.suffix: |
| 409 | + return None |
| 410 | + |
| 411 | + outline_label = infer_output_outline_label( |
| 412 | + dod, |
| 413 | + concrete_target, |
| 414 | + project_root=self.context.project_root, |
| 415 | + todo_label=next_pending or "", |
| 416 | + ) |
| 417 | + if next_pending and _todo_is_mutation_step(next_pending): |
| 418 | + first_line = ( |
| 419 | + f"Continue `{next_pending}` by creating `{concrete_target.name}`." |
| 420 | + ) |
| 421 | + else: |
| 422 | + first_line = f"Create `{concrete_target.name}` now." |
| 423 | + |
| 424 | + lines = [ |
| 425 | + first_line, |
| 426 | + self._mutation_tool_scaffold(concrete_target, tool_name="write"), |
| 427 | + ] |
| 428 | + if outline_label: |
| 429 | + lines.append( |
| 430 | + f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
| 431 | + ) |
| 432 | + if _should_encourage_initial_version( |
| 433 | + target=concrete_target, |
| 434 | + has_confirmed_output_file_progress=True, |
| 435 | + has_confirmed_substantive_output_file_progress=False, |
| 436 | + ): |
| 437 | + lines.append( |
| 438 | + "Write a compact but real initial version of this file now, then refine or expand it in later edits." |
| 439 | + ) |
| 440 | + lines.append( |
| 441 | + "No narration, no TodoWrite, no rereads, and no empty response; emit the mutation tool call now." |
| 442 | + ) |
| 443 | + return "\n".join( |
| 444 | + [ |
| 445 | + "[EMPTY ASSISTANT RESPONSE]", |
| 446 | + ( |
| 447 | + "Your last response was empty " |
| 448 | + f"(retry {retry_number}/{max_empty_retries}). Emit the exact next mutation now." |
| 449 | + ), |
| 450 | + *[f"- {line}" for line in lines], |
| 451 | + ] |
| 452 | + ) |
| 453 | + |
| 371 | 454 | def _payload_retry_lines(self, dod: DefinitionOfDone | None) -> list[str]: |
| 372 | 455 | recovery_context = self.context.recovery_context |
| 373 | 456 | if recovery_context is None or not recovery_context.attempts: |