Python · 101643 bytes Raw Blame History
1 """Tests for response-repair helpers on RuntimeContext."""
2
3 from __future__ import annotations
4
5 import json
6 from pathlib import Path
7 from types import SimpleNamespace
8
9 import pytest
10
11 from loader.llm.base import Message, Role, ToolCall
12 from loader.runtime.context import RuntimeContext
13 from loader.runtime.dod import create_definition_of_done
14 from loader.runtime.path_display import display_runtime_path
15 from loader.runtime.permissions import (
16 PermissionMode,
17 build_permission_policy,
18 load_permission_rules,
19 )
20 from loader.runtime.recovery import RecoveryContext
21 from loader.runtime.repair import ResponseRepairer
22 from loader.tools.base import create_default_registry
23 from tests.helpers.runtime_harness import ScriptedBackend
24
25
26 class FakeSession:
27 def __init__(self) -> None:
28 self.messages = []
29
30 def append(self, message) -> None:
31 self.messages.append(message)
32
33
34 class FakeCodeFilter:
35 def reset(self) -> None:
36 return None
37
38
39 class FakeSafeguards:
40 def __init__(self) -> None:
41 self.action_tracker = object()
42 self.validator = object()
43 self.code_filter = FakeCodeFilter()
44
45 def filter_stream_chunk(self, content: str) -> str:
46 return content
47
48 def filter_complete_content(self, content: str) -> str:
49 return content
50
51 def should_steer(self) -> bool:
52 return False
53
54 def get_steering_message(self) -> str | None:
55 return None
56
57 def record_response(self, content: str) -> None:
58 return None
59
60 def detect_text_loop(self, content: str) -> tuple[bool, str]:
61 return False, ""
62
63 def detect_loop(self) -> tuple[bool, str]:
64 return False, ""
65
66
67 def build_context(
68 *,
69 temp_dir: Path,
70 use_react: bool,
71 ) -> RuntimeContext:
72 registry = create_default_registry(temp_dir)
73 registry.configure_workspace_root(temp_dir)
74 rule_status = load_permission_rules(temp_dir)
75 policy = build_permission_policy(
76 active_mode=PermissionMode.WORKSPACE_WRITE,
77 workspace_root=temp_dir,
78 tool_requirements=registry.get_tool_requirements(),
79 rules=rule_status.rules,
80 )
81 session = FakeSession()
82 return RuntimeContext(
83 project_root=temp_dir,
84 backend=ScriptedBackend(),
85 registry=registry,
86 session=session, # type: ignore[arg-type]
87 config=SimpleNamespace(force_react=use_react),
88 capability_profile=SimpleNamespace(supports_native_tools=not use_react), # type: ignore[arg-type]
89 project_context=None,
90 permission_policy=policy,
91 permission_config_status=rule_status,
92 workflow_mode="execute",
93 safeguards=FakeSafeguards(),
94 )
95
96
97 def test_response_repairer_uses_runtime_parser_for_bracket_tool_fallback(
98 temp_dir: Path,
99 ) -> None:
100 context = build_context(
101 temp_dir=temp_dir,
102 use_react=False,
103 )
104 repairer = ResponseRepairer(context)
105
106 analysis = repairer.analyze_response(
107 content="I need clarification.",
108 response_content='[calls askuserquestion tool with: question="Which path?"]',
109 tool_calls=[],
110 extracted_iterations=0,
111 max_extracted_iterations=3,
112 )
113
114 assert analysis.tool_calls == [
115 ToolCall(
116 id="call_0",
117 name="AskUserQuestion",
118 arguments={"question": "Which path?"},
119 )
120 ]
121 assert analysis.tool_source == "raw_text"
122 assert analysis.clear_stream is True
123
124
125 def test_response_repairer_recovers_todowrite_from_runtime_registry(
126 temp_dir: Path,
127 ) -> None:
128 context = build_context(
129 temp_dir=temp_dir,
130 use_react=False,
131 )
132 repairer = ResponseRepairer(context)
133
134 analysis = repairer.analyze_response(
135 content="I'll track the work first.",
136 response_content=json.dumps(
137 {
138 "name": "TodoWrite",
139 "arguments": {
140 "todos": [
141 {
142 "content": "Run tests",
143 "active_form": "Running tests",
144 "status": "in_progress",
145 }
146 ]
147 },
148 }
149 ),
150 tool_calls=[],
151 extracted_iterations=0,
152 max_extracted_iterations=3,
153 )
154
155 assert analysis.tool_source == "raw_text"
156 assert analysis.clear_stream is True
157 assert analysis.tool_calls == [
158 ToolCall(
159 id="call_0",
160 name="TodoWrite",
161 arguments={
162 "todos": [
163 {
164 "content": "Run tests",
165 "active_form": "Running tests",
166 "status": "in_progress",
167 }
168 ]
169 },
170 )
171 ]
172
173
174 def test_response_repairer_fails_honestly_when_raw_tool_budget_is_exhausted(
175 temp_dir: Path,
176 ) -> None:
177 context = build_context(
178 temp_dir=temp_dir,
179 use_react=False,
180 )
181 repairer = ResponseRepairer(context)
182
183 analysis = repairer.analyze_response(
184 content=json.dumps(
185 {
186 "name": "read",
187 "arguments": {"file_path": "README.md"},
188 }
189 ),
190 response_content=json.dumps(
191 {
192 "name": "read",
193 "arguments": {"file_path": "README.md"},
194 }
195 ),
196 tool_calls=[],
197 extracted_iterations=3,
198 max_extracted_iterations=3,
199 )
200
201 assert analysis.should_stop is True
202 assert analysis.final_response == (
203 "I couldn't safely continue because the model kept emitting raw-text "
204 "tool calls instead of proper tool invocations. Please try again or "
205 "switch to a different backend/model."
206 )
207 assert analysis.failure == "raw-text tool recovery budget exhausted"
208 assert "Let me know if you'd like me to continue" not in analysis.final_response
209
210
211 def test_empty_response_retry_message_surfaces_missing_planned_artifacts_and_working_note(
212 temp_dir: Path,
213 ) -> None:
214 context = build_context(
215 temp_dir=temp_dir,
216 use_react=False,
217 )
218 repairer = ResponseRepairer(context)
219 implementation_plan = temp_dir / "implementation.md"
220 implementation_plan.write_text(
221 "\n".join(
222 [
223 "# Implementation Plan",
224 "",
225 "## File Changes",
226 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
227 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
228 "",
229 ]
230 )
231 )
232 first_artifact = temp_dir / "guides" / "nginx" / "index.html"
233 first_artifact.parent.mkdir(parents=True)
234 first_artifact.write_text("<html></html>\n")
235
236 dod = create_definition_of_done("Create a multi-file nginx guide.")
237 dod.implementation_plan = str(implementation_plan)
238 dod.touched_files.append(str(first_artifact))
239 dod.completed_items.append("Create the main index.html file")
240 dod.pending_items.append("Create each chapter file in sequence")
241
242 context.session.append(
243 SimpleNamespace(
244 role="tool",
245 content=(
246 "Observation [notepad_write_working]: Result: "
247 "- [2026-04-21T19:17:34Z] Creating fifth chapter file: Advanced configurations"
248 ),
249 )
250 )
251
252 decision = repairer.handle_empty_response(
253 task="Create a multi-file nginx guide.",
254 original_task=None,
255 empty_retry_count=1,
256 max_empty_retries=2,
257 dod=dod,
258 )
259
260 assert decision.should_continue is True
261 assert decision.retry_message is not None
262 assert "Latest working note: Creating fifth chapter file: Advanced configurations" in decision.retry_message
263 assert "Confirmed touched files: `index.html`" in decision.retry_message
264 assert "Confirmed completed work: Create the main index.html file" in decision.retry_message
265 assert "Next pending item: Create each chapter file in sequence" in decision.retry_message
266 assert "Continue from the confirmed progress below instead of restarting." in decision.retry_message
267
268
269 def test_empty_response_retry_during_html_quality_repair_shrinks_mutation(
270 temp_dir: Path,
271 ) -> None:
272 context = build_context(
273 temp_dir=temp_dir,
274 use_react=False,
275 )
276 repairer = ResponseRepairer(context)
277 guide = temp_dir / "guides" / "nginx"
278 chapters = guide / "chapters"
279 chapters.mkdir(parents=True)
280 first_chapter = chapters / "01-introduction.html"
281 second_chapter = chapters / "02-installation.html"
282 first_chapter.write_text("<html><body><h1>Intro</h1></body></html>\n")
283 second_chapter.write_text("<html><body><h1>Install</h1></body></html>\n")
284 context.session.append(
285 Message(
286 role=Role.USER,
287 content=(
288 "Repair focus:\n"
289 f"- Improve `{first_chapter}`: insufficient structured content "
290 "(13 blocks, expected at least 18).\n"
291 f"- Improve `{second_chapter}`: thin content "
292 "(514 text chars, expected at least 1758).\n"
293 f"- Immediate next step: edit `{first_chapter}` with a substantial "
294 "expansion or replacement that satisfies its listed quality issue.\n"
295 ),
296 )
297 )
298 dod = create_definition_of_done("Create an equally thorough HTML guide.")
299 dod.touched_files = [str(first_chapter), str(second_chapter)]
300 dod.pending_items.append("Create chapter 1: Introduction to Nginx")
301
302 decision = repairer.handle_empty_response(
303 task="Create an equally thorough HTML guide.",
304 original_task=None,
305 empty_retry_count=1,
306 max_empty_retries=2,
307 dod=dod,
308 )
309
310 assert decision.should_continue is True
311 assert decision.retry_message is not None
312 assert "while repairing HTML content quality" in decision.retry_message
313 assert (
314 f"Active quality repair target: `{first_chapter.resolve(strict=False)}`"
315 in decision.retry_message
316 )
317 assert "Shrink the next step" in decision.retry_message
318 assert "4-6 substantive sections" in decision.retry_message
319 assert "do not attempt a giant full-page rewrite from memory" in decision.retry_message
320 assert "No narration, no TodoWrite, no final summary" in decision.retry_message
321 assert f"`{second_chapter.resolve(strict=False)}`" in decision.retry_message
322
323
324 def test_empty_response_retry_uses_exact_anchor_after_stale_quality_repair_context(
325 temp_dir: Path,
326 ) -> None:
327 context = build_context(
328 temp_dir=temp_dir,
329 use_react=False,
330 )
331 repairer = ResponseRepairer(context)
332 guide = temp_dir / "guides" / "nginx"
333 chapters = guide / "chapters"
334 chapters.mkdir(parents=True)
335 chapter = chapters / "05-load-balancing.html"
336 chapter.write_text("<html><body><h1>Load Balancing</h1></body></html>\n")
337 context.session.append(
338 Message(
339 role=Role.USER,
340 content=(
341 "Repair focus:\n"
342 f"- Improve `{chapter}`: thin content "
343 "(846 text chars, expected at least 1758).\n"
344 f"- Immediate next step: edit `{chapter}`.\n"
345 ),
346 )
347 )
348 context.session.append(
349 Message(
350 role=Role.TOOL,
351 content=(
352 "Observation [edit]: Error: Failed to complete the operation "
353 f"after 2 attempts for {chapter}. old_string not found in file."
354 ),
355 )
356 )
357 dod = create_definition_of_done("Create an equally thorough HTML guide.")
358 dod.touched_files = [str(chapter)]
359
360 decision = repairer.handle_empty_response(
361 task="Create an equally thorough HTML guide.",
362 original_task=None,
363 empty_retry_count=1,
364 max_empty_retries=2,
365 dod=dod,
366 )
367
368 assert decision.should_continue is True
369 assert decision.retry_message is not None
370 assert (
371 "Use exactly one `edit(file_path=..., old_string=..., new_string=...)`"
372 in decision.retry_message
373 )
374 assert "Use this exact `old_string` value from the current file" in decision.retry_message
375 assert "```html\n</body></html>\n```" in decision.retry_message
376 assert "Do not call `read`, `patch`, `write`, TodoWrite" in decision.retry_message
377
378
379 def test_empty_response_retry_forces_write_for_structural_html_repair(
380 temp_dir: Path,
381 ) -> None:
382 context = build_context(
383 temp_dir=temp_dir,
384 use_react=False,
385 )
386 repairer = ResponseRepairer(context)
387 chapter = temp_dir / "guides" / "nginx" / "chapters" / "08-troubleshooting.html"
388 chapter.parent.mkdir(parents=True)
389 chapter.write_text(
390 "<!DOCTYPE html><html><body><h1>Troubleshooting</h1></body></html>\n"
391 "<p>Trailing content.</p>\n"
392 )
393 context.session.append(
394 Message(
395 role=Role.USER,
396 content=(
397 "Repair focus:\n"
398 f"- Improve `{chapter}`: content appears after closing </html>.\n"
399 f"- Immediate next step: replace `{chapter}` with one complete valid HTML document.\n"
400 ),
401 )
402 )
403 dod = create_definition_of_done("Create an equally thorough HTML guide.")
404 dod.touched_files = [str(chapter)]
405
406 decision = repairer.handle_empty_response(
407 task="Create an equally thorough HTML guide.",
408 original_task=None,
409 empty_retry_count=1,
410 max_empty_retries=2,
411 dod=dod,
412 )
413
414 assert decision.should_continue is True
415 assert decision.retry_message is not None
416 assert "content appears after closing </html>" in decision.retry_message
417 assert "Use exactly one `write(file_path=..., content=...)`" in decision.retry_message
418 assert "exactly one closing `</body>` tag" in decision.retry_message
419
420
421 def test_empty_response_retry_mentions_write_can_create_missing_parent_directories(
422 temp_dir: Path,
423 ) -> None:
424 context = build_context(
425 temp_dir=temp_dir,
426 use_react=False,
427 )
428 repairer = ResponseRepairer(context)
429
430 guide_root = temp_dir / "guides" / "nginx"
431 index_path = guide_root / "index.html"
432
433 implementation_plan = temp_dir / "implementation.md"
434 implementation_plan.write_text(
435 "\n".join(
436 [
437 "# Implementation Plan",
438 "",
439 "## File Changes",
440 f"- `{index_path}`",
441 "",
442 ]
443 )
444 )
445
446 dod = create_definition_of_done("Create a multi-file nginx guide.")
447 dod.implementation_plan = str(implementation_plan)
448 dod.pending_items.extend(
449 [
450 "Create nginx guide directory structure",
451 "Write main index.html for nginx guide",
452 ]
453 )
454
455 decision = repairer.handle_empty_response(
456 task="Create a multi-file nginx guide.",
457 original_task=None,
458 empty_retry_count=1,
459 max_empty_retries=2,
460 dod=dod,
461 )
462
463 assert decision.should_continue is True
464 assert decision.retry_message is not None
465 assert (
466 "Resume with this exact next step: create `index.html`."
467 in decision.retry_message
468 )
469 assert (
470 "Prefer one `write(content=...)` call for "
471 f"`{display_runtime_path(index_path)}` before more research."
472 in decision.retry_message
473 )
474 assert (
475 "The `write` tool can create that file's parent directories automatically, so do the write in one step instead of stopping for a separate mkdir."
476 in decision.retry_message
477 )
478 assert (
479 f'Emit this tool shape now: `write(file_path="{display_runtime_path(index_path)}", content="...")`.'
480 in decision.retry_message
481 )
482 assert (
483 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
484 in decision.retry_message
485 )
486 assert (
487 "No narration, no TodoWrite, no rereads, and no empty response; emit the mutation tool call now."
488 in decision.retry_message
489 )
490
491
492 def test_empty_response_retry_preserves_blocked_html_asset_correction(
493 temp_dir: Path,
494 ) -> None:
495 context = build_context(
496 temp_dir=temp_dir,
497 use_react=False,
498 )
499 repairer = ResponseRepairer(context)
500
501 target = temp_dir / "guides" / "nginx" / "chapters" / "07-security.html"
502 context.session.append(
503 Message(
504 role=Role.ASSISTANT,
505 content="",
506 tool_calls=[
507 ToolCall(
508 id="write-security",
509 name="write",
510 arguments={
511 "file_path": str(target),
512 "content": '<link rel="stylesheet" href="../styles.css">',
513 },
514 )
515 ],
516 )
517 )
518 context.session.append(
519 Message.tool_result_message(
520 tool_call_id="write-security",
521 display_content=(
522 "[Blocked - HTML local asset references do not exist] Suggestion: "
523 "Use only existing local assets for non-HTML href values. "
524 "Missing local asset href(s): ../styles.css. Remove the asset link, "
525 "create the referenced asset first, inline the styling/content, or point "
526 "the href at an existing local file."
527 ),
528 result_content=(
529 "[Blocked - HTML local asset references do not exist] Suggestion: "
530 "Missing local asset href(s): ../styles.css."
531 ),
532 is_error=True,
533 )
534 )
535
536 dod = create_definition_of_done("Create a multi-file nginx guide.")
537 dod.pending_items.append("Create individual chapter files")
538
539 decision = repairer.handle_empty_response(
540 task="Create a multi-file nginx guide.",
541 original_task=None,
542 empty_retry_count=1,
543 max_empty_retries=2,
544 dod=dod,
545 )
546
547 assert decision.should_continue is True
548 assert decision.retry_message is not None
549 assert "after a blocked HTML asset reference" in decision.retry_message
550 assert f"`{display_runtime_path(target)}`" in decision.retry_message
551 assert "Missing local asset href(s): `../styles.css`." in decision.retry_message
552 assert "remove the entire stylesheet/image/script tag" in decision.retry_message
553 assert "Do not include those missing href values" in decision.retry_message
554 assert "Create individual chapter files" not in decision.retry_message
555
556
557 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
558 temp_dir: Path,
559 ) -> None:
560 context = build_context(
561 temp_dir=temp_dir,
562 use_react=False,
563 )
564 repairer = ResponseRepairer(context)
565
566 guide_root = temp_dir / "guides" / "nginx"
567 chapters_path = guide_root / "chapters"
568 index_path = guide_root / "index.html"
569
570 implementation_plan = temp_dir / "implementation.md"
571 implementation_plan.write_text(
572 "\n".join(
573 [
574 "# Implementation Plan",
575 "",
576 "## File Changes",
577 f"- `{chapters_path}/`",
578 f"- `{index_path}`",
579 "",
580 ]
581 )
582 )
583
584 dod = create_definition_of_done("Create a multi-file nginx guide.")
585 dod.implementation_plan = str(implementation_plan)
586 dod.pending_items.extend(
587 [
588 "Create the nginx directory structure",
589 "Create the main index.html file for nginx guide",
590 ]
591 )
592
593 decision = repairer.handle_empty_response(
594 task="Create a multi-file nginx guide.",
595 original_task=None,
596 empty_retry_count=1,
597 max_empty_retries=2,
598 dod=dod,
599 )
600
601 assert decision.should_continue is True
602 assert decision.retry_message is not None
603 assert (
604 "Resume with this exact next step: continue `Create the nginx directory structure` "
605 "by creating `chapters/`."
606 in decision.retry_message
607 )
608 assert (
609 "Prefer one concrete directory-creation step for "
610 f"`{display_runtime_path(chapters_path)}` before more research."
611 in decision.retry_message
612 )
613 expected_command = f"mkdir -p {display_runtime_path(chapters_path)}"
614 assert (
615 f'Emit this tool shape now: `bash(command="{expected_command}")`.'
616 in decision.retry_message
617 )
618 assert f'write(file_path="{display_runtime_path(chapters_path)}"' not in decision.retry_message
619
620
621 def test_empty_response_retry_uses_home_relative_path_for_home_artifacts(
622 temp_dir: Path,
623 monkeypatch: pytest.MonkeyPatch,
624 ) -> None:
625 monkeypatch.setenv("HOME", str(temp_dir.resolve(strict=False)))
626 context = build_context(
627 temp_dir=temp_dir,
628 use_react=False,
629 )
630 repairer = ResponseRepairer(context)
631
632 guide_root = temp_dir / "Loader" / "guides" / "nginx"
633 index_path = guide_root / "index.html"
634
635 implementation_plan = temp_dir / "implementation.md"
636 implementation_plan.write_text(
637 "\n".join(
638 [
639 "# Implementation Plan",
640 "",
641 "## File Changes",
642 f"- `{index_path}`",
643 "",
644 ]
645 )
646 )
647
648 dod = create_definition_of_done("Create a multi-file nginx guide.")
649 dod.implementation_plan = str(implementation_plan)
650 dod.pending_items.extend(
651 [
652 "Create nginx guide directory structure",
653 "Write main index.html for nginx guide",
654 ]
655 )
656
657 decision = repairer.handle_empty_response(
658 task="Create a multi-file nginx guide.",
659 original_task=None,
660 empty_retry_count=1,
661 max_empty_retries=2,
662 dod=dod,
663 )
664
665 assert decision.should_continue is True
666 assert decision.retry_message is not None
667 assert "`~/Loader/guides/nginx/index.html`" in decision.retry_message
668 assert (
669 'Emit this tool shape now: `write(file_path="~/Loader/guides/nginx/index.html", content="...")`.'
670 in decision.retry_message
671 )
672 assert (
673 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
674 in decision.retry_message
675 )
676
677
678 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
679 temp_dir: Path,
680 ) -> None:
681 context = build_context(
682 temp_dir=temp_dir,
683 use_react=False,
684 )
685 repairer = ResponseRepairer(context)
686
687 guide_root = temp_dir / "guides" / "nginx"
688 chapters = guide_root / "chapters"
689 chapters.mkdir(parents=True)
690 index_path = guide_root / "index.html"
691 first_chapter = chapters / "01-introduction.html"
692 second_chapter = chapters / "02-installation.html"
693 index_path.write_text("<html></html>\n")
694 first_chapter.write_text("<h1>Intro</h1>\n")
695
696 implementation_plan = temp_dir / "implementation.md"
697 implementation_plan.write_text(
698 "\n".join(
699 [
700 "# Implementation Plan",
701 "",
702 "## File Changes",
703 f"- `{index_path}`",
704 f"- `{first_chapter}`",
705 f"- `{second_chapter}`",
706 "",
707 ]
708 )
709 )
710
711 dod = create_definition_of_done("Create a multi-file nginx guide.")
712 dod.implementation_plan = str(implementation_plan)
713 dod.touched_files.extend([str(index_path), str(first_chapter)])
714 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
715
716 context.recovery_context = RecoveryContext(
717 original_tool="write",
718 original_args={"file_path": "", "content": "<html></html>\n"},
719 )
720 context.recovery_context.add_attempt(
721 "write",
722 {"file_path": "", "content": "<html></html>\n"},
723 "Empty file path",
724 )
725
726 decision = repairer.handle_empty_response(
727 task="Create a multi-file nginx guide.",
728 original_task=None,
729 empty_retry_count=1,
730 max_empty_retries=2,
731 dod=dod,
732 )
733
734 assert decision.should_continue is True
735 assert decision.retry_message is not None
736 assert (
737 "Last tool failure: resend `write` for "
738 f"`{display_runtime_path(second_chapter)}` with a valid `file_path` and real `content`."
739 in decision.retry_message
740 )
741 assert "Do not leave `file_path` empty" in decision.retry_message
742 assert (
743 f'Emit this tool shape now: `write(file_path="{display_runtime_path(second_chapter)}", content="...")`.'
744 in decision.retry_message
745 )
746
747
748 def test_empty_response_retry_respects_discovery_first_pending_step(
749 temp_dir: Path,
750 ) -> None:
751 context = build_context(
752 temp_dir=temp_dir,
753 use_react=False,
754 )
755 repairer = ResponseRepairer(context)
756
757 implementation_plan = temp_dir / "implementation.md"
758 implementation_plan.write_text(
759 "\n".join(
760 [
761 "# Implementation Plan",
762 "",
763 "## File Changes",
764 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
765 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
766 "",
767 ]
768 )
769 )
770
771 dod = create_definition_of_done("Create a multi-file nginx guide.")
772 dod.implementation_plan = str(implementation_plan)
773 dod.pending_items.extend(
774 [
775 "First, examine the existing fortran guide structure and content to understand the format",
776 "Create the nginx directory structure",
777 "Develop the main index.html file for the nginx guide",
778 ]
779 )
780
781 context.session.append(
782 SimpleNamespace(
783 role="tool",
784 content=(
785 "Observation [notepad_write_working]: Result: "
786 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
787 ),
788 )
789 )
790
791 decision = repairer.handle_empty_response(
792 task="Create a multi-file nginx guide.",
793 original_task=None,
794 empty_retry_count=1,
795 max_empty_retries=2,
796 dod=dod,
797 )
798
799 assert decision.should_continue is True
800 assert decision.retry_message is not None
801 assert (
802 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
803 in decision.retry_message
804 )
805 assert "one concrete evidence-gathering tool call" in decision.retry_message
806 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
807
808
809 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
810 temp_dir: Path,
811 ) -> None:
812 context = build_context(
813 temp_dir=temp_dir,
814 use_react=False,
815 )
816 repairer = ResponseRepairer(context)
817
818 guide_root = temp_dir / "guides" / "nginx"
819 chapters = guide_root / "chapters"
820 chapters.mkdir(parents=True)
821 index_path = guide_root / "index.html"
822 chapter_one = chapters / "01-getting-started.html"
823 chapter_two = chapters / "02-installation.html"
824 chapter_three = chapters / "03-first-website.html"
825 chapter_four = chapters / "04-configuration-basics.html"
826 index_path.write_text("<html></html>\n")
827 chapter_one.write_text("<h1>One</h1>\n")
828 chapter_two.write_text("<h1>Two</h1>\n")
829 chapter_three.write_text("<h1>Three</h1>\n")
830
831 implementation_plan = temp_dir / "implementation.md"
832 implementation_plan.write_text(
833 "\n".join(
834 [
835 "# Implementation Plan",
836 "",
837 "## File Changes",
838 f"- `{guide_root}/`",
839 f"- `{chapters}/`",
840 f"- `{index_path}`",
841 f"- `{chapter_one}`",
842 f"- `{chapter_two}`",
843 f"- `{chapter_three}`",
844 f"- `{chapter_four}`",
845 "",
846 ]
847 )
848 )
849
850 dod = create_definition_of_done("Create a multi-file nginx guide.")
851 dod.implementation_plan = str(implementation_plan)
852 dod.touched_files.extend(
853 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
854 )
855 dod.completed_items.extend(
856 [
857 "Create the directory structure for the new nginx guide",
858 "Create the main index.html file with proper structure",
859 ]
860 )
861 dod.pending_items.append("Create each chapter file in sequence")
862
863 decision = repairer.handle_empty_response(
864 task="Create a multi-file nginx guide.",
865 original_task=None,
866 empty_retry_count=3,
867 max_empty_retries=2,
868 dod=dod,
869 )
870
871 assert decision.should_continue is True
872 assert decision.retry_message is not None
873 assert "retry 3/4" in decision.retry_message
874 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
875
876
877 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
878 temp_dir: Path,
879 ) -> None:
880 context = build_context(
881 temp_dir=temp_dir,
882 use_react=False,
883 )
884 repairer = ResponseRepairer(context)
885
886 implementation_plan = temp_dir / "implementation.md"
887 implementation_plan.write_text(
888 "\n".join(
889 [
890 "# Implementation Plan",
891 "",
892 "## File Changes",
893 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
894 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
895 "",
896 ]
897 )
898 )
899
900 dod = create_definition_of_done("Create a multi-file nginx guide.")
901 dod.implementation_plan = str(implementation_plan)
902 dod.pending_items.append("Develop the main index.html file for the nginx guide")
903
904 decision = repairer.handle_empty_response(
905 task="Create a multi-file nginx guide.",
906 original_task=None,
907 empty_retry_count=3,
908 max_empty_retries=2,
909 dod=dod,
910 )
911
912 assert decision.should_continue is True
913 assert decision.retry_message is not None
914 assert "retry 3/4" in decision.retry_message
915 assert "Next missing planned artifact: `index.html`" in decision.retry_message
916 assert (
917 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
918 "by creating `index.html`."
919 in decision.retry_message
920 )
921
922
923 def test_empty_response_retry_budget_extends_after_setup_directories_exist(
924 temp_dir: Path,
925 ) -> None:
926 context = build_context(
927 temp_dir=temp_dir,
928 use_react=False,
929 )
930 repairer = ResponseRepairer(context)
931
932 guide_root = temp_dir / "guides" / "nginx"
933 chapters = guide_root / "chapters"
934 chapters.mkdir(parents=True)
935 index_path = guide_root / "index.html"
936
937 implementation_plan = temp_dir / "implementation.md"
938 implementation_plan.write_text(
939 "\n".join(
940 [
941 "# Implementation Plan",
942 "",
943 "## File Changes",
944 f"- `{guide_root}/`",
945 f"- `{index_path}`",
946 f"- `{chapters}/`",
947 "",
948 ]
949 )
950 )
951
952 dod = create_definition_of_done("Create a multi-file nginx guide.")
953 dod.implementation_plan = str(implementation_plan)
954 dod.completed_items.extend(
955 [
956 "First, examine the existing Fortran guide structure to understand the format and depth",
957 "Create the new nginx guide directory structure",
958 ]
959 )
960 dod.pending_items.append("Develop the main index.html file for the nginx guide")
961
962 decision = repairer.handle_empty_response(
963 task="Create a multi-file nginx guide.",
964 original_task=None,
965 empty_retry_count=5,
966 max_empty_retries=2,
967 dod=dod,
968 )
969
970 assert decision.should_continue is True
971 assert decision.retry_message is not None
972 assert "retry 5/6" in decision.retry_message
973 assert "Next missing planned artifact: `index.html`" in decision.retry_message
974 assert (
975 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
976 "by creating `index.html`."
977 in decision.retry_message
978 )
979
980
981 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
982 temp_dir: Path,
983 ) -> None:
984 context = build_context(
985 temp_dir=temp_dir,
986 use_react=False,
987 )
988 repairer = ResponseRepairer(context)
989
990 guide_root = temp_dir / "guides" / "nginx"
991 chapters = guide_root / "chapters"
992 guide_root.mkdir(parents=True)
993 chapters.mkdir()
994 index_path = guide_root / "index.html"
995 index_path.write_text("<html></html>\n")
996
997 implementation_plan = temp_dir / "implementation.md"
998 implementation_plan.write_text(
999 "\n".join(
1000 [
1001 "# Implementation Plan",
1002 "",
1003 "## File Changes",
1004 f"- `{chapters}/`",
1005 f"- `{index_path}`",
1006 "",
1007 ]
1008 )
1009 )
1010
1011 dod = create_definition_of_done("Create a multi-file nginx guide.")
1012 dod.implementation_plan = str(implementation_plan)
1013 dod.touched_files.append(str(index_path))
1014 dod.completed_items.extend(
1015 [
1016 "Create the new nginx guide directory structure",
1017 "Develop the main index.html file with proper structure",
1018 ]
1019 )
1020 dod.pending_items.append("Create 01-introduction.html")
1021
1022 decision = repairer.handle_empty_response(
1023 task="Create a multi-file nginx guide.",
1024 original_task=None,
1025 empty_retry_count=5,
1026 max_empty_retries=2,
1027 dod=dod,
1028 )
1029
1030 assert decision.should_continue is True
1031 assert decision.retry_message is not None
1032 assert "retry 5/6" in decision.retry_message
1033 assert (
1034 "Resume with this exact next step: continue `Create 01-introduction.html` "
1035 "by creating `01-introduction.html`."
1036 in decision.retry_message
1037 )
1038 assert 'Emit this tool shape now: `write(file_path="' in decision.retry_message
1039 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
1040
1041
1042 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
1043 temp_dir: Path,
1044 ) -> None:
1045 context = build_context(
1046 temp_dir=temp_dir,
1047 use_react=False,
1048 )
1049 context.session.messages.append(
1050 SimpleNamespace(
1051 content=(
1052 "Observation [notepad_write_working]: Result: "
1053 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
1054 )
1055 )
1056 )
1057 repairer = ResponseRepairer(context)
1058
1059 guide_root = temp_dir / "guides" / "nginx"
1060 chapters = guide_root / "chapters"
1061 chapters.mkdir(parents=True)
1062 index_path = guide_root / "index.html"
1063 chapter_one = chapters / "01-getting-started.html"
1064 chapter_two = chapters / "02-installation.html"
1065 chapter_three = chapters / "03-first-website.html"
1066 chapter_four = chapters / "04-configuration-basics.html"
1067 chapter_five = chapters / "05-advanced-features.html"
1068 index_path.write_text("<html></html>\n")
1069 chapter_one.write_text("<h1>One</h1>\n")
1070 chapter_two.write_text("<h1>Two</h1>\n")
1071 chapter_three.write_text("<h1>Three</h1>\n")
1072 chapter_four.write_text("<h1>Four</h1>\n")
1073
1074 implementation_plan = temp_dir / "implementation.md"
1075 implementation_plan.write_text(
1076 "\n".join(
1077 [
1078 "# Implementation Plan",
1079 "",
1080 "## File Changes",
1081 f"- `{guide_root}/`",
1082 f"- `{chapters}/`",
1083 f"- `{index_path}`",
1084 f"- `{chapter_one}`",
1085 f"- `{chapter_two}`",
1086 f"- `{chapter_three}`",
1087 f"- `{chapter_four}`",
1088 f"- `{chapter_five}`",
1089 "",
1090 ]
1091 )
1092 )
1093
1094 dod = create_definition_of_done("Create a multi-file nginx guide.")
1095 dod.implementation_plan = str(implementation_plan)
1096 dod.touched_files.extend(
1097 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
1098 )
1099 dod.completed_items.extend(
1100 [
1101 "Create the directory structure for the new nginx guide",
1102 "Create the main index.html file with proper structure",
1103 ]
1104 )
1105 dod.pending_items.append("Create each chapter file in sequence")
1106
1107 decision = repairer.handle_empty_response(
1108 task="Create a multi-file nginx guide.",
1109 original_task=None,
1110 empty_retry_count=3,
1111 max_empty_retries=2,
1112 dod=dod,
1113 )
1114
1115 assert decision.should_continue is True
1116 assert decision.retry_message is not None
1117 assert "Continue from the exact next step below." in decision.retry_message
1118 assert "Latest working note:" not in decision.retry_message
1119 assert "Confirmed completed work:" not in decision.retry_message
1120 assert "Next pending item:" not in decision.retry_message
1121
1122
1123 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
1124 temp_dir: Path,
1125 ) -> None:
1126 context = build_context(
1127 temp_dir=temp_dir,
1128 use_react=False,
1129 )
1130 repairer = ResponseRepairer(context)
1131
1132 guide_root = temp_dir / "guides" / "nginx"
1133 chapters = guide_root / "chapters"
1134 chapters.mkdir(parents=True)
1135 index_path = guide_root / "index.html"
1136 index_path.write_text("<html></html>\n")
1137
1138 implementation_plan = temp_dir / "implementation.md"
1139 implementation_plan.write_text(
1140 "\n".join(
1141 [
1142 "# Implementation Plan",
1143 "",
1144 "## File Changes",
1145 f"- `{guide_root}/`",
1146 f"- `{chapters}/`",
1147 f"- `{index_path}`",
1148 f"- `{chapters / '02-installation.html'}`",
1149 "",
1150 ]
1151 )
1152 )
1153
1154 dod = create_definition_of_done("Create a multi-file nginx guide.")
1155 dod.implementation_plan = str(implementation_plan)
1156 dod.touched_files.append(str(index_path))
1157 dod.pending_items.append("Write the introduction chapter")
1158
1159 decision = repairer.handle_empty_response(
1160 task="Create a multi-file nginx guide.",
1161 original_task=None,
1162 empty_retry_count=1,
1163 max_empty_retries=2,
1164 dod=dod,
1165 )
1166
1167 assert decision.should_continue is True
1168 assert decision.retry_message is not None
1169 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
1170 assert (
1171 "Resume with this exact next step: continue `Write the introduction chapter` "
1172 "by creating the next output file under `chapters/`."
1173 in decision.retry_message
1174 )
1175 assert (
1176 "Prefer one concrete `write` call for a file inside "
1177 f"`{display_runtime_path(chapters)}` before more research."
1178 in decision.retry_message
1179 )
1180
1181
1182 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
1183 temp_dir: Path,
1184 ) -> None:
1185 context = build_context(
1186 temp_dir=temp_dir,
1187 use_react=False,
1188 )
1189 repairer = ResponseRepairer(context)
1190
1191 guide_root = temp_dir / "guides" / "nginx"
1192 chapters = guide_root / "chapters"
1193 guide_root.mkdir(parents=True)
1194 chapters.mkdir()
1195 chapter_one = chapters / "01-introduction.html"
1196 index_path = guide_root / "index.html"
1197
1198 implementation_plan = temp_dir / "implementation.md"
1199 implementation_plan.write_text(
1200 "\n".join(
1201 [
1202 "# Implementation Plan",
1203 "",
1204 "## File Changes",
1205 f"- `{guide_root}/`",
1206 f"- `{index_path}`",
1207 f"- `{chapters}/`",
1208 f"- `{chapter_one}`",
1209 "",
1210 ]
1211 )
1212 )
1213
1214 dod = create_definition_of_done("Create a multi-file nginx guide.")
1215 dod.implementation_plan = str(implementation_plan)
1216 dod.completed_items.extend(
1217 [
1218 "First, examine the existing Fortran guide structure to understand the format and depth",
1219 "Create the new nginx guide directory structure",
1220 ]
1221 )
1222 dod.pending_items.append("Develop the main index.html file with proper structure")
1223
1224 decision = repairer.handle_empty_response(
1225 task="Create a multi-file nginx guide.",
1226 original_task=None,
1227 empty_retry_count=2,
1228 max_empty_retries=2,
1229 dod=dod,
1230 )
1231
1232 assert decision.should_continue is True
1233 assert decision.retry_message is not None
1234 assert (
1235 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
1236 in decision.retry_message
1237 )
1238 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1239 assert "Prefer one `write(content=...)` call" in decision.retry_message
1240 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
1241
1242
1243 def test_empty_response_retry_adds_root_html_starter_shape_for_first_index_write(
1244 temp_dir: Path,
1245 ) -> None:
1246 context = build_context(
1247 temp_dir=temp_dir,
1248 use_react=False,
1249 )
1250 repairer = ResponseRepairer(context)
1251
1252 guide_root = temp_dir / "guides" / "nginx"
1253 chapters = guide_root / "chapters"
1254 guide_root.mkdir(parents=True)
1255 chapters.mkdir()
1256 index_path = guide_root / "index.html"
1257 chapter_one = chapters / "01-introduction.html"
1258 index_path.write_text(
1259 "\n".join(
1260 [
1261 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1262 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1263 "",
1264 ]
1265 )
1266 )
1267
1268 implementation_plan = temp_dir / "implementation.md"
1269 implementation_plan.write_text(
1270 "\n".join(
1271 [
1272 "# Implementation Plan",
1273 "",
1274 "## File Changes",
1275 f"- `{guide_root}/`",
1276 f"- `{index_path}`",
1277 f"- `{chapters}/`",
1278 f"- `{chapter_one}`",
1279 "",
1280 ]
1281 )
1282 )
1283
1284 dod = create_definition_of_done("Create a multi-file nginx guide.")
1285 dod.implementation_plan = str(implementation_plan)
1286 dod.completed_items.extend(
1287 [
1288 "First, examine the existing Fortran guide structure to understand the format and depth",
1289 "Create the new nginx guide directory structure",
1290 ]
1291 )
1292 dod.pending_items.append("Develop the main index.html file with proper structure")
1293
1294 decision = repairer.handle_empty_response(
1295 task="Create a multi-file nginx guide.",
1296 original_task=None,
1297 empty_retry_count=2,
1298 max_empty_retries=2,
1299 dod=dod,
1300 )
1301
1302 assert decision.should_continue is True
1303 assert decision.retry_message is not None
1304 assert (
1305 "If you get stuck, start with `<!DOCTYPE html>`, `<title>Nginx Guide</title>`, "
1306 "`<h1>Nginx Guide</h1>`, a short intro paragraph, and a linked chapter list that "
1307 "points at the guide pages you will create under `chapters/`."
1308 in decision.retry_message
1309 )
1310 assert "../index.html" not in decision.retry_message
1311
1312
1313 def test_repeated_first_index_retry_includes_root_html_payload_shape(
1314 temp_dir: Path,
1315 ) -> None:
1316 context = build_context(
1317 temp_dir=temp_dir,
1318 use_react=False,
1319 )
1320 repairer = ResponseRepairer(context)
1321
1322 guide_root = temp_dir / "guides" / "nginx"
1323 chapters = guide_root / "chapters"
1324 guide_root.mkdir(parents=True)
1325 chapters.mkdir()
1326 index_path = guide_root / "index.html"
1327 chapter_one = chapters / "01-introduction.html"
1328
1329 implementation_plan = temp_dir / "implementation.md"
1330 implementation_plan.write_text(
1331 "\n".join(
1332 [
1333 "# Implementation Plan",
1334 "",
1335 "## File Changes",
1336 f"- `{guide_root}/`",
1337 f"- `{index_path}`",
1338 f"- `{chapters}/`",
1339 f"- `{chapter_one}`",
1340 "",
1341 ]
1342 )
1343 )
1344
1345 dod = create_definition_of_done("Create a multi-file nginx guide.")
1346 dod.implementation_plan = str(implementation_plan)
1347 dod.completed_items.extend(
1348 [
1349 "First, examine the existing Fortran guide structure to understand the format and depth",
1350 "Create the new nginx guide directory structure",
1351 ]
1352 )
1353 dod.pending_items.append("Develop the main index.html file with proper structure")
1354
1355 decision = repairer.handle_empty_response(
1356 task="Create a multi-file nginx guide.",
1357 original_task=None,
1358 empty_retry_count=5,
1359 max_empty_retries=6,
1360 dod=dod,
1361 )
1362
1363 assert decision.should_continue is True
1364 assert decision.retry_message is not None
1365 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1366 assert "<title>Nginx Guide</title>" in decision.retry_message
1367 assert 'href="chapters/01-introduction.html"' in decision.retry_message
1368 assert "../index.html" not in decision.retry_message
1369 assert "Starter overview" not in decision.retry_message
1370 assert "go here" not in decision.retry_message
1371
1372
1373 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
1374 temp_dir: Path,
1375 ) -> None:
1376 context = build_context(
1377 temp_dir=temp_dir,
1378 use_react=False,
1379 )
1380 repairer = ResponseRepairer(context)
1381
1382 guide_root = temp_dir / "guides" / "nginx"
1383 chapters = guide_root / "chapters"
1384 guide_root.mkdir(parents=True)
1385 chapters.mkdir()
1386 index_path = guide_root / "index.html"
1387 chapter_one = chapters / "01-introduction.html"
1388
1389 implementation_plan = temp_dir / "implementation.md"
1390 implementation_plan.write_text(
1391 "\n".join(
1392 [
1393 "# Implementation Plan",
1394 "",
1395 "## File Changes",
1396 f"- `{guide_root}/`",
1397 f"- `{chapters}/`",
1398 f"- `{index_path}`",
1399 f"- `{chapter_one}`",
1400 "",
1401 ]
1402 )
1403 )
1404
1405 dod = create_definition_of_done("Create a multi-file nginx guide.")
1406 dod.implementation_plan = str(implementation_plan)
1407 dod.completed_items.extend(
1408 [
1409 "First, examine the existing Fortran guide structure to understand the format and depth",
1410 "Create the new nginx guide directory structure",
1411 ]
1412 )
1413 dod.pending_items.append("Develop the main index.html file with proper structure")
1414
1415 decision = repairer.handle_empty_response(
1416 task="Create a multi-file nginx guide.",
1417 original_task=None,
1418 empty_retry_count=4,
1419 max_empty_retries=4,
1420 dod=dod,
1421 )
1422
1423 assert decision.should_continue is True
1424 assert decision.retry_message is not None
1425 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1426 assert (
1427 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
1428 "by creating `index.html`."
1429 in decision.retry_message
1430 )
1431 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
1432 assert "Remaining planned artifacts:" not in decision.retry_message
1433 assert (
1434 "Next observed output pattern under `chapters/`: `01-introduction.html`"
1435 not in decision.retry_message
1436 )
1437
1438
1439 def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_step(
1440 temp_dir: Path,
1441 ) -> None:
1442 context = build_context(
1443 temp_dir=temp_dir,
1444 use_react=False,
1445 )
1446 repairer = ResponseRepairer(context)
1447
1448 guide_root = temp_dir / "guides" / "nginx"
1449 chapters = guide_root / "chapters"
1450 chapters.mkdir(parents=True)
1451 index_path = guide_root / "index.html"
1452 index_path.write_text(
1453 "\n".join(
1454 [
1455 "<html>",
1456 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1457 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1458 "</html>",
1459 ]
1460 )
1461 + "\n"
1462 )
1463
1464 implementation_plan = temp_dir / "implementation.md"
1465 implementation_plan.write_text(
1466 "\n".join(
1467 [
1468 "# Implementation Plan",
1469 "",
1470 "## File Changes",
1471 f"- `{guide_root}/`",
1472 f"- `{chapters}/`",
1473 f"- `{index_path}`",
1474 "",
1475 ]
1476 )
1477 )
1478
1479 dod = create_definition_of_done("Create a multi-file nginx guide.")
1480 dod.implementation_plan = str(implementation_plan)
1481 dod.touched_files.append(str(index_path))
1482 dod.completed_items.append("Develop the main index.html file with proper structure")
1483 dod.pending_items.append("Create chapter files with content and structure")
1484
1485 decision = repairer.handle_empty_response(
1486 task="Create a multi-file nginx guide.",
1487 original_task=None,
1488 empty_retry_count=3,
1489 max_empty_retries=4,
1490 dod=dod,
1491 )
1492
1493 assert decision.should_continue is True
1494 assert decision.retry_message is not None
1495 assert "Next missing planned artifact:" not in decision.retry_message
1496 assert (
1497 "Resume with this exact next step: continue `Create chapter files with content and structure` "
1498 "by creating `01-introduction.html`."
1499 in decision.retry_message
1500 )
1501 assert (
1502 'Emit this tool shape now: `write(file_path="'
1503 in decision.retry_message
1504 )
1505 assert (
1506 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
1507 not in decision.retry_message
1508 )
1509 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
1510 assert "Follow the same full-payload one-file-at-a-time write pattern" not in decision.retry_message
1511 assert "Remaining planned artifacts:" not in decision.retry_message
1512 assert "Next pending item:" not in decision.retry_message
1513
1514
1515 def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapter_step(
1516 temp_dir: Path,
1517 ) -> None:
1518 context = build_context(
1519 temp_dir=temp_dir,
1520 use_react=False,
1521 )
1522 repairer = ResponseRepairer(context)
1523
1524 guide_root = temp_dir / "guides" / "nginx"
1525 chapters = guide_root / "chapters"
1526 chapters.mkdir(parents=True)
1527 index_path = guide_root / "index.html"
1528 chapter_one = chapters / "01-introduction.html"
1529 chapter_two = chapters / "02-installation.html"
1530 index_path.write_text(
1531 "\n".join(
1532 [
1533 "<html>",
1534 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1535 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1536 "</html>",
1537 ]
1538 )
1539 + "\n"
1540 )
1541 chapter_one.write_text("<h1>Introduction</h1>\n")
1542
1543 implementation_plan = temp_dir / "implementation.md"
1544 implementation_plan.write_text(
1545 "\n".join(
1546 [
1547 "# Implementation Plan",
1548 "",
1549 "## File Changes",
1550 f"- `{guide_root}/`",
1551 f"- `{chapters}/`",
1552 f"- `{index_path}`",
1553 "",
1554 ]
1555 )
1556 )
1557
1558 dod = create_definition_of_done("Create a multi-file nginx guide.")
1559 dod.implementation_plan = str(implementation_plan)
1560 dod.touched_files.extend([str(index_path), str(chapter_one)])
1561 dod.completed_items.extend(
1562 [
1563 "Develop the main index.html file with proper structure",
1564 "Create first chapter file (01-introduction.html)",
1565 ]
1566 )
1567 dod.pending_items.append("Create chapter files following the established pattern")
1568
1569 decision = repairer.handle_empty_response(
1570 task="Create a multi-file nginx guide.",
1571 original_task=None,
1572 empty_retry_count=1,
1573 max_empty_retries=2,
1574 dod=dod,
1575 )
1576
1577 assert decision.should_continue is True
1578 assert decision.retry_message is not None
1579 assert "Next pending item:" not in decision.retry_message
1580 assert (
1581 "Resume with this exact next step: continue `Create chapter files following the established pattern` "
1582 "by creating `02-installation.html`."
1583 in decision.retry_message
1584 )
1585 assert (
1586 "It is the next concrete output needed to continue `Create chapter files following the established pattern`."
1587 in decision.retry_message
1588 )
1589 assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message
1590 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
1591
1592
1593 def test_empty_response_retry_keeps_first_substantive_retry_lean(
1594 temp_dir: Path,
1595 ) -> None:
1596 context = build_context(
1597 temp_dir=temp_dir,
1598 use_react=False,
1599 )
1600 repairer = ResponseRepairer(context)
1601
1602 guide_root = temp_dir / "guides" / "nginx"
1603 chapters = guide_root / "chapters"
1604 chapters.mkdir(parents=True)
1605 index_path = guide_root / "index.html"
1606 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1607 reference_chapter.parent.mkdir(parents=True)
1608 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1609 index_path.write_text(
1610 "\n".join(
1611 [
1612 "<html>",
1613 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1614 "</html>",
1615 ]
1616 )
1617 + "\n"
1618 )
1619 context.session.append(
1620 Message(
1621 role=Role.ASSISTANT,
1622 content="",
1623 tool_calls=[
1624 ToolCall(
1625 id="call_ref",
1626 name="read",
1627 arguments={"file_path": str(reference_chapter)},
1628 )
1629 ],
1630 )
1631 )
1632
1633 implementation_plan = temp_dir / "implementation.md"
1634 implementation_plan.write_text(
1635 "\n".join(
1636 [
1637 "# Implementation Plan",
1638 "",
1639 "## File Changes",
1640 f"- `{guide_root}/`",
1641 f"- `{chapters}/`",
1642 f"- `{index_path}`",
1643 "",
1644 ]
1645 )
1646 )
1647
1648 dod = create_definition_of_done("Create a multi-file nginx guide.")
1649 dod.implementation_plan = str(implementation_plan)
1650 dod.touched_files.append(str(index_path))
1651 dod.completed_items.append("Develop the main index.html file with proper structure")
1652 dod.pending_items.append("Create chapter files following the established pattern")
1653
1654 decision = repairer.handle_empty_response(
1655 task="Create a multi-file nginx guide.",
1656 original_task=None,
1657 empty_retry_count=1,
1658 max_empty_retries=2,
1659 dod=dod,
1660 )
1661
1662 assert decision.should_continue is True
1663 assert decision.retry_message is not None
1664 assert (
1665 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1666 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1667 "and chapter body content."
1668 in decision.retry_message
1669 )
1670 assert (
1671 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1672 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1673 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1674 in decision.retry_message
1675 )
1676 assert display_runtime_path(reference_chapter) not in decision.retry_message
1677 assert "Reference cues from" not in decision.retry_message
1678 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1679 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1680
1681
1682 def test_late_first_substantive_retry_stays_lean(
1683 temp_dir: Path,
1684 ) -> None:
1685 context = build_context(
1686 temp_dir=temp_dir,
1687 use_react=False,
1688 )
1689 repairer = ResponseRepairer(context)
1690
1691 guide_root = temp_dir / "guides" / "nginx"
1692 chapters = guide_root / "chapters"
1693 chapters.mkdir(parents=True)
1694 index_path = guide_root / "index.html"
1695 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1696 reference_chapter.parent.mkdir(parents=True)
1697 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1698 index_path.write_text(
1699 "\n".join(
1700 [
1701 "<html>",
1702 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1703 "</html>",
1704 ]
1705 )
1706 + "\n"
1707 )
1708 context.session.append(
1709 Message(
1710 role=Role.ASSISTANT,
1711 content="",
1712 tool_calls=[
1713 ToolCall(
1714 id="call_ref",
1715 name="read",
1716 arguments={"file_path": str(reference_chapter)},
1717 )
1718 ],
1719 )
1720 )
1721
1722 implementation_plan = temp_dir / "implementation.md"
1723 implementation_plan.write_text(
1724 "\n".join(
1725 [
1726 "# Implementation Plan",
1727 "",
1728 "## File Changes",
1729 f"- `{guide_root}/`",
1730 f"- `{chapters}/`",
1731 f"- `{index_path}`",
1732 "",
1733 ]
1734 )
1735 )
1736
1737 dod = create_definition_of_done("Create a multi-file nginx guide.")
1738 dod.implementation_plan = str(implementation_plan)
1739 dod.touched_files.append(str(index_path))
1740 dod.completed_items.append("Develop the main index.html file with proper structure")
1741 dod.pending_items.append("Create chapter files following the established pattern")
1742
1743 decision = repairer.handle_empty_response(
1744 task="Create a multi-file nginx guide.",
1745 original_task=None,
1746 empty_retry_count=3,
1747 max_empty_retries=4,
1748 dod=dod,
1749 )
1750
1751 assert decision.should_continue is True
1752 assert decision.retry_message is not None
1753 assert (
1754 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1755 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1756 "and chapter body content."
1757 in decision.retry_message
1758 )
1759 assert (
1760 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1761 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1762 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1763 in decision.retry_message
1764 )
1765 assert display_runtime_path(reference_chapter) not in decision.retry_message
1766 assert "Reference cues from" not in decision.retry_message
1767 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
1768 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1769
1770
1771 def test_repeated_first_substantive_retry_includes_minimal_payload_shape(
1772 temp_dir: Path,
1773 ) -> None:
1774 context = build_context(
1775 temp_dir=temp_dir,
1776 use_react=False,
1777 )
1778 repairer = ResponseRepairer(context)
1779
1780 guide_root = temp_dir / "guides" / "nginx"
1781 chapters = guide_root / "chapters"
1782 chapters.mkdir(parents=True)
1783 index_path = guide_root / "index.html"
1784 index_path.write_text(
1785 "\n".join(
1786 [
1787 "<html>",
1788 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1789 "</html>",
1790 ]
1791 )
1792 + "\n"
1793 )
1794
1795 implementation_plan = temp_dir / "implementation.md"
1796 implementation_plan.write_text(
1797 "\n".join(
1798 [
1799 "# Implementation Plan",
1800 "",
1801 "## File Changes",
1802 f"- `{guide_root}/`",
1803 f"- `{chapters}/`",
1804 f"- `{index_path}`",
1805 "",
1806 ]
1807 )
1808 )
1809
1810 dod = create_definition_of_done("Create a multi-file nginx guide.")
1811 dod.implementation_plan = str(implementation_plan)
1812 dod.touched_files.append(str(index_path))
1813 dod.completed_items.append("Develop the main index.html file with proper structure")
1814 dod.pending_items.append("Create chapter files following the established pattern")
1815
1816 decision = repairer.handle_empty_response(
1817 task="Create a multi-file nginx guide.",
1818 original_task=None,
1819 empty_retry_count=5,
1820 max_empty_retries=6,
1821 dod=dod,
1822 )
1823
1824 assert decision.should_continue is True
1825 assert decision.retry_message is not None
1826 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1827 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1828 assert "../index.html" in decision.retry_message
1829 assert "Starter content" not in decision.retry_message
1830 assert "Key concepts go here" not in decision.retry_message
1831 assert "Practical steps go here" not in decision.retry_message
1832
1833
1834 def test_empty_response_retry_surfaces_minimal_child_html_payload_earlier_after_progress(
1835 temp_dir: Path,
1836 ) -> None:
1837 context = build_context(
1838 temp_dir=temp_dir,
1839 use_react=False,
1840 )
1841 repairer = ResponseRepairer(context)
1842
1843 guide_root = temp_dir / "guides" / "nginx"
1844 chapters = guide_root / "chapters"
1845 guide_root.mkdir(parents=True)
1846 chapters.mkdir()
1847 index_path = guide_root / "index.html"
1848 chapter_one = chapters / "01-introduction.html"
1849 index_path.write_text(
1850 "\n".join(
1851 [
1852 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1853 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1854 "",
1855 ]
1856 )
1857 )
1858
1859 implementation_plan = temp_dir / "implementation.md"
1860 implementation_plan.write_text(
1861 "\n".join(
1862 [
1863 "# Implementation Plan",
1864 "",
1865 "## File Changes",
1866 f"- `{guide_root}/`",
1867 f"- `{index_path}`",
1868 f"- `{chapters}/`",
1869 f"- `{chapter_one}`",
1870 "",
1871 ]
1872 )
1873 )
1874
1875 dod = create_definition_of_done("Create a multi-file nginx guide.")
1876 dod.implementation_plan = str(implementation_plan)
1877 dod.touched_files.append(str(index_path))
1878 dod.completed_items.append("Develop the main index.html file with proper structure")
1879 dod.pending_items.append("Create first chapter file (01-introduction.html)")
1880
1881 decision = repairer.handle_empty_response(
1882 task="Create a multi-file nginx guide.",
1883 original_task=None,
1884 empty_retry_count=3,
1885 max_empty_retries=6,
1886 dod=dod,
1887 )
1888
1889 assert decision.should_continue is True
1890 assert decision.retry_message is not None
1891 assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message
1892 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1893 assert "Starter content" not in decision.retry_message
1894 assert "go here" not in decision.retry_message
1895
1896
1897 def test_final_empty_response_retry_uses_short_exact_write_call(
1898 temp_dir: Path,
1899 ) -> None:
1900 context = build_context(
1901 temp_dir=temp_dir,
1902 use_react=False,
1903 )
1904 repairer = ResponseRepairer(context)
1905
1906 guide_root = temp_dir / "guides" / "nginx"
1907 chapters = guide_root / "chapters"
1908 index_path = guide_root / "index.html"
1909 chapter_one = chapters / "01-introduction.html"
1910 chapters.mkdir(parents=True)
1911 index_path.write_text("<h1>Nginx Guide</h1>\n")
1912 implementation_plan = temp_dir / "implementation.md"
1913 implementation_plan.write_text(
1914 "\n".join(
1915 [
1916 "# Implementation Plan",
1917 "",
1918 "## File Changes",
1919 f"- `{index_path}`",
1920 f"- `{chapter_one}`",
1921 "",
1922 ]
1923 )
1924 )
1925
1926 dod = create_definition_of_done("Create a multi-file nginx guide.")
1927 dod.implementation_plan = str(implementation_plan)
1928 dod.touched_files.append(str(index_path))
1929 dod.completed_items.append("Develop the main index.html file with proper structure")
1930 dod.pending_items.append("Create first chapter file (01-introduction.html)")
1931
1932 decision = repairer.handle_empty_response(
1933 task="Create a multi-file nginx guide.",
1934 original_task=None,
1935 empty_retry_count=10,
1936 max_empty_retries=6,
1937 dod=dod,
1938 )
1939
1940 assert decision.should_continue is True
1941 assert decision.retry_message is not None
1942 assert "Emit one tool call only" in decision.retry_message
1943 assert 'write(file_path="' in decision.retry_message
1944 assert "01-introduction.html" in decision.retry_message
1945 assert "No prose, no TodoWrite, no reads." in decision.retry_message
1946 assert "If blanking continues" not in decision.retry_message
1947 assert len(decision.retry_message) < 1000
1948
1949
1950 def test_first_substantive_retry_activates_on_first_empty_turn(
1951 temp_dir: Path,
1952 ) -> None:
1953 context = build_context(
1954 temp_dir=temp_dir,
1955 use_react=False,
1956 )
1957 repairer = ResponseRepairer(context)
1958
1959 guide_root = temp_dir / "guides" / "nginx"
1960 chapters = guide_root / "chapters"
1961 chapters.mkdir(parents=True)
1962 index_path = guide_root / "index.html"
1963 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1964 reference_chapter.parent.mkdir(parents=True)
1965 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1966 index_path.write_text(
1967 "\n".join(
1968 [
1969 "<html>",
1970 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1971 "</html>",
1972 ]
1973 )
1974 + "\n"
1975 )
1976 context.session.append(
1977 Message(
1978 role=Role.ASSISTANT,
1979 content="",
1980 tool_calls=[
1981 ToolCall(
1982 id="call_ref",
1983 name="read",
1984 arguments={"file_path": str(reference_chapter)},
1985 )
1986 ],
1987 )
1988 )
1989
1990 implementation_plan = temp_dir / "implementation.md"
1991 implementation_plan.write_text(
1992 "\n".join(
1993 [
1994 "# Implementation Plan",
1995 "",
1996 "## File Changes",
1997 f"- `{guide_root}/`",
1998 f"- `{chapters}/`",
1999 f"- `{index_path}`",
2000 "",
2001 ]
2002 )
2003 )
2004
2005 dod = create_definition_of_done("Create a multi-file nginx guide.")
2006 dod.implementation_plan = str(implementation_plan)
2007 dod.touched_files.append(str(index_path))
2008 dod.completed_items.append("Develop the main index.html file with proper structure")
2009 dod.pending_items.append("Create the nginx chapters content")
2010
2011 decision = repairer.handle_empty_response(
2012 task="Create a multi-file nginx guide.",
2013 original_task=None,
2014 empty_retry_count=1,
2015 max_empty_retries=4,
2016 dod=dod,
2017 )
2018
2019 assert decision.should_continue is True
2020 assert decision.retry_message is not None
2021 assert "Emit this tool shape now" in decision.retry_message
2022 assert "01-introduction.html" in decision.retry_message
2023 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
2024
2025
2026 def test_late_first_substantive_retry_trims_context_to_core_write_cues(
2027 temp_dir: Path,
2028 ) -> None:
2029 context = build_context(
2030 temp_dir=temp_dir,
2031 use_react=False,
2032 )
2033 repairer = ResponseRepairer(context)
2034
2035 guide_root = temp_dir / "guides" / "nginx"
2036 chapters = guide_root / "chapters"
2037 chapters.mkdir(parents=True)
2038 index_path = guide_root / "index.html"
2039 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
2040 reference_chapter.parent.mkdir(parents=True)
2041 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
2042 index_path.write_text(
2043 "\n".join(
2044 [
2045 "<html>",
2046 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
2047 "</html>",
2048 ]
2049 )
2050 + "\n"
2051 )
2052 context.session.append(
2053 Message(
2054 role=Role.ASSISTANT,
2055 content="",
2056 tool_calls=[
2057 ToolCall(
2058 id="call_ref",
2059 name="read",
2060 arguments={"file_path": str(reference_chapter)},
2061 )
2062 ],
2063 )
2064 )
2065
2066 implementation_plan = temp_dir / "implementation.md"
2067 implementation_plan.write_text(
2068 "\n".join(
2069 [
2070 "# Implementation Plan",
2071 "",
2072 "## File Changes",
2073 f"- `{guide_root}/`",
2074 f"- `{chapters}/`",
2075 f"- `{index_path}`",
2076 "",
2077 ]
2078 )
2079 )
2080
2081 dod = create_definition_of_done("Create a multi-file nginx guide.")
2082 dod.implementation_plan = str(implementation_plan)
2083 dod.touched_files.append(str(index_path))
2084 dod.completed_items.append("Develop the main index.html file with proper structure")
2085 dod.pending_items.append("Create chapter files following the established pattern")
2086
2087 decision = repairer.handle_empty_response(
2088 task="Create a multi-file nginx guide.",
2089 original_task=None,
2090 empty_retry_count=6,
2091 max_empty_retries=6,
2092 dod=dod,
2093 )
2094
2095 assert decision.should_continue is True
2096 assert decision.retry_message is not None
2097 assert (
2098 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
2099 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
2100 "and chapter body content."
2101 in decision.retry_message
2102 )
2103 assert (
2104 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
2105 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
2106 "of `<h2>` sections with short body text, and a back link to `../index.html`."
2107 in decision.retry_message
2108 )
2109 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
2110 assert (
2111 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
2112 "as the starting pattern for this new file, then adapt the content to the current target."
2113 not in decision.retry_message
2114 )
2115 assert "Reference cues from" not in decision.retry_message
2116 assert "If blanking continues, use this minimal HTML starter" not in decision.retry_message
2117 assert "Write a compact but real initial version of this file now" not in decision.retry_message
2118
2119
2120 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
2121 temp_dir: Path,
2122 ) -> None:
2123 context = build_context(
2124 temp_dir=temp_dir,
2125 use_react=False,
2126 )
2127 repairer = ResponseRepairer(context)
2128
2129 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
2130 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
2131 nginx_root.mkdir(parents=True)
2132 fortran_root.mkdir(parents=True)
2133 reference_index = fortran_root / "index.html"
2134 reference_index.write_text("<html>fortran</html>\n")
2135 output_index = nginx_root / "index.html"
2136
2137 implementation_plan = temp_dir / "implementation.md"
2138 implementation_plan.write_text(
2139 "\n".join(
2140 [
2141 "# Implementation Plan",
2142 "",
2143 "## File Changes",
2144 f"- `{output_index}`",
2145 f"- `{nginx_root / 'chapters'}/`",
2146 f"- `{reference_index}`",
2147 "",
2148 ]
2149 )
2150 )
2151
2152 dod = create_definition_of_done("Create a multi-file nginx guide.")
2153 dod.implementation_plan = str(implementation_plan)
2154 dod.touched_files.append(str(reference_index))
2155 dod.completed_items.append(
2156 "First, examine the existing Fortran guide structure and content"
2157 )
2158 dod.pending_items.append("Develop the nginx index.html file")
2159
2160 decision = repairer.handle_empty_response(
2161 task="Create a multi-file nginx guide.",
2162 original_task=None,
2163 empty_retry_count=2,
2164 max_empty_retries=2,
2165 dod=dod,
2166 )
2167
2168 assert decision.should_continue is True
2169 assert decision.retry_message is not None
2170 assert (
2171 "Resume with this exact next step: continue `Develop the nginx index.html file` "
2172 f"by creating `{output_index.name}`."
2173 in decision.retry_message
2174 )
2175 assert (
2176 f'Emit this tool shape now: `write(file_path="{display_runtime_path(output_index)}", content="...")`.'
2177 in decision.retry_message
2178 )
2179 assert str(reference_index) not in decision.retry_message
2180
2181
2182 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
2183 temp_dir: Path,
2184 ) -> None:
2185 context = build_context(
2186 temp_dir=temp_dir,
2187 use_react=False,
2188 )
2189 repairer = ResponseRepairer(context)
2190
2191 guide_root = temp_dir / "guides" / "nginx"
2192 chapters = guide_root / "chapters"
2193 chapters.mkdir(parents=True)
2194 index_path = guide_root / "index.html"
2195 index_path.write_text(
2196 "\n".join(
2197 [
2198 "<html>",
2199 '<a href="chapters/introduction.html">Introduction</a>',
2200 '<a href="chapters/installation.html">Installation</a>',
2201 "</html>",
2202 ]
2203 )
2204 + "\n"
2205 )
2206
2207 implementation_plan = temp_dir / "implementation.md"
2208 implementation_plan.write_text(
2209 "\n".join(
2210 [
2211 "# Implementation Plan",
2212 "",
2213 "## File Changes",
2214 f"- `{guide_root}/`",
2215 f"- `{chapters}/`",
2216 f"- `{index_path}`",
2217 f"- `{chapters / '02-installation.html'}`",
2218 "",
2219 ]
2220 )
2221 )
2222
2223 dod = create_definition_of_done("Create a multi-file nginx guide.")
2224 dod.implementation_plan = str(implementation_plan)
2225 dod.touched_files.append(str(index_path))
2226 dod.pending_items.append("Write the introduction chapter")
2227
2228 decision = repairer.handle_empty_response(
2229 task="Create a multi-file nginx guide.",
2230 original_task=None,
2231 empty_retry_count=1,
2232 max_empty_retries=2,
2233 dod=dod,
2234 )
2235
2236 assert decision.should_continue is True
2237 assert decision.retry_message is not None
2238 assert (
2239 "Resume with this exact next step: continue `Write the introduction chapter` "
2240 "by creating `introduction.html`."
2241 in decision.retry_message
2242 )
2243 assert "Next declared output under `chapters/`" not in decision.retry_message
2244 assert (
2245 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "introduction.html")}", content="...")`.'
2246 in decision.retry_message
2247 )
2248
2249
2250 def test_empty_response_retry_names_missing_declared_child_after_directory_exists(
2251 temp_dir: Path,
2252 ) -> None:
2253 context = build_context(
2254 temp_dir=temp_dir,
2255 use_react=False,
2256 )
2257 repairer = ResponseRepairer(context)
2258
2259 guide_root = temp_dir / "guides" / "nginx"
2260 chapters = guide_root / "chapters"
2261 chapters.mkdir(parents=True)
2262 index_path = guide_root / "index.html"
2263 chapter_four = chapters / "04-reverse-proxy.html"
2264 chapter_five = chapters / "05-load-balancing.html"
2265 chapter_six = chapters / "06-security.html"
2266 index_path.write_text(
2267 "\n".join(
2268 [
2269 "<html>",
2270 '<a href="chapters/04-reverse-proxy.html">Reverse Proxy</a>',
2271 '<a href="chapters/05-load-balancing.html">Load Balancing</a>',
2272 '<a href="chapters/06-security.html">Security</a>',
2273 "</html>",
2274 ]
2275 )
2276 + "\n"
2277 )
2278 chapter_four.write_text("<html><h1>Reverse Proxy</h1></html>\n")
2279 chapter_five.write_text("<html><h1>Load Balancing</h1></html>\n")
2280
2281 implementation_plan = temp_dir / "implementation.md"
2282 implementation_plan.write_text(
2283 "\n".join(
2284 [
2285 "# Implementation Plan",
2286 "",
2287 "## File Changes",
2288 f"- `{guide_root}/`",
2289 f"- `{chapters}/`",
2290 f"- `{index_path}`",
2291 "",
2292 ]
2293 )
2294 )
2295
2296 dod = create_definition_of_done("Create a multi-file nginx guide.")
2297 dod.implementation_plan = str(implementation_plan)
2298 dod.touched_files.extend([str(index_path), str(chapter_four), str(chapter_five)])
2299 dod.completed_items.append("Create load balancing chapter")
2300 dod.pending_items.extend(["Complete the requested work", "Collect verification evidence"])
2301
2302 decision = repairer.handle_empty_response(
2303 task="Create a multi-file nginx guide.",
2304 original_task=None,
2305 empty_retry_count=2,
2306 max_empty_retries=2,
2307 dod=dod,
2308 )
2309
2310 assert decision.should_continue is True
2311 assert decision.retry_message is not None
2312 assert "Resume with this exact next step: create `06-security.html`." in decision.retry_message
2313 assert "It is the next missing declared output" in decision.retry_message
2314 assert (
2315 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapter_six)}", content="...")`.'
2316 in decision.retry_message
2317 )
2318
2319
2320 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
2321 temp_dir: Path,
2322 ) -> None:
2323 context = build_context(
2324 temp_dir=temp_dir,
2325 use_react=False,
2326 )
2327 repairer = ResponseRepairer(context)
2328
2329 guide_root = temp_dir / "guides" / "nginx"
2330 chapters = guide_root / "chapters"
2331 chapters.mkdir(parents=True)
2332 index_path = guide_root / "index.html"
2333 chapter_one = chapters / "01-introduction.html"
2334 index_path.write_text("<html></html>\n")
2335 chapter_one.write_text("<html></html>\n")
2336
2337 implementation_plan = temp_dir / "implementation.md"
2338 implementation_plan.write_text(
2339 "\n".join(
2340 [
2341 "# Implementation Plan",
2342 "",
2343 "## File Changes",
2344 f"- `{guide_root}/`",
2345 f"- `{chapters}/`",
2346 f"- `{index_path}`",
2347 f"- `{chapters / '02-installation.html'}`",
2348 "",
2349 ]
2350 )
2351 )
2352
2353 dod = create_definition_of_done("Create a multi-file nginx guide.")
2354 dod.implementation_plan = str(implementation_plan)
2355 dod.touched_files.extend([str(index_path), str(chapter_one)])
2356 dod.completed_items.extend(
2357 [
2358 "Create index.html for nginx guide",
2359 "Create first chapter file (01-introduction.html)",
2360 ]
2361 )
2362 dod.pending_items.append("Create second chapter file (02-installation.html)")
2363
2364 decision = repairer.handle_empty_response(
2365 task="Create a multi-file nginx guide.",
2366 original_task=None,
2367 empty_retry_count=2,
2368 max_empty_retries=2,
2369 dod=dod,
2370 )
2371
2372 assert decision.should_continue is True
2373 assert decision.retry_message is not None
2374 assert (
2375 "Resume with this exact next step: continue `Create second chapter file "
2376 "(02-installation.html)` by creating `02-installation.html`."
2377 in decision.retry_message
2378 )
2379 assert (
2380 "Prefer one `write(content=...)` call for "
2381 f"`{display_runtime_path(chapters / '02-installation.html')}` "
2382 "before more research."
2383 in decision.retry_message
2384 )
2385 assert "Do not return another working note or empty response" in decision.retry_message
2386
2387
2388 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
2389 temp_dir: Path,
2390 ) -> None:
2391 context = build_context(
2392 temp_dir=temp_dir,
2393 use_react=False,
2394 )
2395 repairer = ResponseRepairer(context)
2396
2397 guide_root = temp_dir / "guides" / "nginx"
2398 chapters = guide_root / "chapters"
2399 chapters.mkdir(parents=True)
2400 index_path = guide_root / "index.html"
2401 chapter_one = chapters / "01-introduction.html"
2402 index_path.write_text(
2403 "\n".join(
2404 [
2405 "<html>",
2406 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
2407 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
2408 "</html>",
2409 ]
2410 )
2411 + "\n"
2412 )
2413 chapter_one.write_text("<html></html>\n")
2414
2415 implementation_plan = temp_dir / "implementation.md"
2416 implementation_plan.write_text(
2417 "\n".join(
2418 [
2419 "# Implementation Plan",
2420 "",
2421 "## File Changes",
2422 f"- `{guide_root}/`",
2423 f"- `{chapters}/`",
2424 f"- `{index_path}`",
2425 f"- `{chapters / '02-installation.html'}`",
2426 "",
2427 ]
2428 )
2429 )
2430
2431 dod = create_definition_of_done("Create a multi-file nginx guide.")
2432 dod.implementation_plan = str(implementation_plan)
2433 dod.touched_files.extend([str(index_path), str(chapter_one)])
2434 dod.completed_items.extend(
2435 [
2436 "Create index.html for nginx guide",
2437 "Create Chapter 1: Introduction to NGINX Tool",
2438 ]
2439 )
2440 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
2441
2442 decision = repairer.handle_empty_response(
2443 task="Create a multi-file nginx guide.",
2444 original_task=None,
2445 empty_retry_count=2,
2446 max_empty_retries=2,
2447 dod=dod,
2448 )
2449
2450 assert decision.should_continue is True
2451 assert decision.retry_message is not None
2452 assert (
2453 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
2454 "by creating `02-installation.html`."
2455 in decision.retry_message
2456 )
2457 assert (
2458 "Prefer one `write(content=...)` call for "
2459 f"`{display_runtime_path(chapters / '02-installation.html')}` "
2460 "before more research."
2461 in decision.retry_message
2462 )
2463 assert (
2464 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "02-installation.html")}", content="...")`.'
2465 in decision.retry_message
2466 )
2467 assert (
2468 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
2469 "so it matches the current guide structure."
2470 in decision.retry_message
2471 )
2472
2473
2474 def test_late_chapter_retry_reuses_existing_sibling_html_structure(
2475 temp_dir: Path,
2476 ) -> None:
2477 context = build_context(
2478 temp_dir=temp_dir,
2479 use_react=False,
2480 )
2481 repairer = ResponseRepairer(context)
2482
2483 guide_root = temp_dir / "guides" / "nginx"
2484 chapters = guide_root / "chapters"
2485 chapters.mkdir(parents=True)
2486 index_path = guide_root / "index.html"
2487 chapter_one = chapters / "01-introduction.html"
2488 chapter_two = chapters / "02-installation.html"
2489 chapter_three = chapters / "03-basic-configuration.html"
2490 index_path.write_text(
2491 "\n".join(
2492 [
2493 "<html>",
2494 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
2495 '<a href="chapters/02-installation.html">Chapter 2: Installation</a>',
2496 '<a href="chapters/03-basic-configuration.html">Chapter 3: Basic Configuration</a>',
2497 "</html>",
2498 ]
2499 )
2500 + "\n"
2501 )
2502 chapter_one.write_text("<html><body><h1>Chapter 1</h1></body></html>\n")
2503 chapter_two.write_text("<html><body><h1>Chapter 2</h1></body></html>\n")
2504
2505 implementation_plan = temp_dir / "implementation.md"
2506 implementation_plan.write_text(
2507 "\n".join(
2508 [
2509 "# Implementation Plan",
2510 "",
2511 "## File Changes",
2512 f"- `{guide_root}/`",
2513 f"- `{chapters}/`",
2514 f"- `{index_path}`",
2515 f"- `{chapter_one}`",
2516 f"- `{chapter_two}`",
2517 f"- `{chapter_three}`",
2518 "",
2519 ]
2520 )
2521 )
2522
2523 dod = create_definition_of_done("Create a multi-file nginx guide.")
2524 dod.implementation_plan = str(implementation_plan)
2525 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2526 dod.completed_items.extend(
2527 [
2528 "Create index.html for nginx guide",
2529 "Create first chapter file (01-introduction.html)",
2530 "Create second chapter file (02-installation.html)",
2531 ]
2532 )
2533 dod.pending_items.append("Create third chapter file (03-basic-configuration.html)")
2534
2535 decision = repairer.handle_empty_response(
2536 task="Create a multi-file nginx guide.",
2537 original_task=None,
2538 empty_retry_count=4,
2539 max_empty_retries=2,
2540 dod=dod,
2541 )
2542
2543 assert decision.should_continue is True
2544 assert decision.retry_message is not None
2545 assert (
2546 "Reuse the overall structure and navigation pattern from "
2547 f"`{display_runtime_path(chapter_two)}` as the starting pattern for "
2548 "`Chapter 3: Basic Configuration`; adapt the title, heading, and body content "
2549 "to the new chapter."
2550 in decision.retry_message
2551 )
2552 assert (
2553 "If you get stuck, start with `<title>Chapter 3: Basic Configuration</title>`, "
2554 "`<h1>Chapter 3: Basic Configuration</h1>`, one introductory paragraph, a couple "
2555 "of `<h2>` sections with short body text, and a back link to `../index.html`."
2556 in decision.retry_message
2557 )
2558
2559
2560 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
2561 temp_dir: Path,
2562 ) -> None:
2563 context = build_context(
2564 temp_dir=temp_dir,
2565 use_react=False,
2566 )
2567 repairer = ResponseRepairer(context)
2568
2569 guide_root = temp_dir / "guides" / "nginx"
2570 chapters = guide_root / "chapters"
2571 chapters.mkdir(parents=True)
2572 chapter_one = chapters / "01-introduction.html"
2573 chapter_one.write_text("<html></html>\n")
2574
2575 implementation_plan = temp_dir / "implementation.md"
2576 implementation_plan.write_text(
2577 "\n".join(
2578 [
2579 "# Implementation Plan",
2580 "",
2581 "## File Changes",
2582 f"- `{guide_root}/`",
2583 f"- `{chapters}/`",
2584 f"- `{guide_root / 'index.html'}`",
2585 f"- `{chapters / '01-introduction.html'}`",
2586 "",
2587 ]
2588 )
2589 )
2590
2591 dod = create_definition_of_done("Create a multi-file nginx guide.")
2592 dod.implementation_plan = str(implementation_plan)
2593 dod.touched_files.append(str(chapter_one))
2594 dod.completed_items.append("Create first chapter file (01-introduction.html)")
2595 dod.pending_items.append("Develop the main index.html file for the nginx guide")
2596
2597 recovery_context = RecoveryContext(
2598 original_tool="write",
2599 original_args={
2600 "file_path": "~/Loader/guides/nginx/index.html",
2601 "content_chars": 1354,
2602 "content_lines": 30,
2603 },
2604 )
2605 recovery_context.add_attempt(
2606 "write",
2607 {
2608 "file_path": "~/Loader/guides/nginx/index.html",
2609 "content_chars": 1354,
2610 "content_lines": 30,
2611 },
2612 "WriteTool.execute() missing 1 required positional argument: 'content'",
2613 )
2614 context.recovery_context = recovery_context
2615
2616 decision = repairer.handle_empty_response(
2617 task="Create a multi-file nginx guide.",
2618 original_task=None,
2619 empty_retry_count=2,
2620 max_empty_retries=2,
2621 dod=dod,
2622 )
2623
2624 assert decision.should_continue is True
2625 assert decision.retry_message is not None
2626 assert "resend `write`" in decision.retry_message
2627 assert "content_chars" in decision.retry_message
2628 assert "index.html" in decision.retry_message
2629
2630
2631 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
2632 temp_dir: Path,
2633 ) -> None:
2634 context = build_context(
2635 temp_dir=temp_dir,
2636 use_react=False,
2637 )
2638 repairer = ResponseRepairer(context)
2639
2640 guide_root = temp_dir / "guides" / "nginx"
2641 chapters = guide_root / "chapters"
2642 chapters.mkdir(parents=True)
2643 index_path = guide_root / "index.html"
2644 chapter_one = chapters / "01-introduction.html"
2645 index_path.write_text(
2646 "\n".join(
2647 [
2648 "<html>",
2649 '<a href="chapters/01-introduction.html">Introduction</a>',
2650 '<a href="chapters/02-installation.html">Installation</a>',
2651 "</html>",
2652 ]
2653 )
2654 + "\n"
2655 )
2656 chapter_one.write_text("<html></html>\n")
2657
2658 implementation_plan = temp_dir / "implementation.md"
2659 implementation_plan.write_text(
2660 "\n".join(
2661 [
2662 "# Implementation Plan",
2663 "",
2664 "## File Changes",
2665 f"- `{guide_root}/`",
2666 f"- `{chapters}/`",
2667 f"- `{index_path}`",
2668 f"- `{chapters / '02-installation.html'}`",
2669 "",
2670 ]
2671 )
2672 )
2673
2674 dod = create_definition_of_done("Create a multi-file nginx guide.")
2675 dod.implementation_plan = str(implementation_plan)
2676 dod.touched_files.extend([str(index_path), str(chapter_one)])
2677 dod.completed_items.extend(
2678 [
2679 "Create index.html for nginx guide",
2680 "Create first chapter file (01-introduction.html)",
2681 ]
2682 )
2683 dod.pending_items.append("Create second chapter file (02-installation.html)")
2684
2685 decision = repairer.handle_empty_response(
2686 task="Create a multi-file nginx guide.",
2687 original_task=None,
2688 empty_retry_count=1,
2689 max_empty_retries=2,
2690 dod=dod,
2691 )
2692
2693 assert decision.should_continue is True
2694 assert decision.retry_message is not None
2695 assert "Continue from the exact next step below." in decision.retry_message
2696 assert "Confirmed completed work:" not in decision.retry_message
2697 assert "Next pending item:" not in decision.retry_message
2698 assert (
2699 "Resume with this exact next step: continue `Create second chapter file "
2700 "(02-installation.html)` by creating `02-installation.html`."
2701 in decision.retry_message
2702 )
2703
2704
2705 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
2706 temp_dir: Path,
2707 ) -> None:
2708 context = build_context(
2709 temp_dir=temp_dir,
2710 use_react=False,
2711 )
2712 repairer = ResponseRepairer(context)
2713
2714 guide_root = temp_dir / "guides" / "nginx"
2715 chapters = guide_root / "chapters"
2716 chapters.mkdir(parents=True)
2717 index_path = guide_root / "index.html"
2718 chapter_one = chapters / "01-introduction.html"
2719 index_path.write_text("<html></html>\n")
2720 chapter_one.write_text("<html></html>\n")
2721
2722 implementation_plan = temp_dir / "implementation.md"
2723 implementation_plan.write_text(
2724 "\n".join(
2725 [
2726 "# Implementation Plan",
2727 "",
2728 "## File Changes",
2729 f"- `{guide_root}/`",
2730 f"- `{chapters}/`",
2731 f"- `{index_path}`",
2732 f"- `{chapter_one}`",
2733 f"- `{chapters / '02-installation.html'}`",
2734 "",
2735 ]
2736 )
2737 )
2738
2739 dod = create_definition_of_done("Create a multi-file nginx guide.")
2740 dod.implementation_plan = str(implementation_plan)
2741 dod.touched_files.extend([str(index_path), str(chapter_one)])
2742 dod.completed_items.extend(
2743 [
2744 "Develop the main index.html file for the nginx guide",
2745 "Create first chapter file (01-introduction.html)",
2746 ]
2747 )
2748 dod.pending_items.extend(
2749 [
2750 "Create the nginx directory structure",
2751 "Create second chapter file (02-installation.html)",
2752 ]
2753 )
2754
2755 decision = repairer.handle_empty_response(
2756 task="Create a multi-file nginx guide.",
2757 original_task=None,
2758 empty_retry_count=1,
2759 max_empty_retries=2,
2760 dod=dod,
2761 )
2762
2763 assert decision.should_continue is True
2764 assert decision.retry_message is not None
2765 assert "Create the nginx directory structure" not in decision.retry_message
2766 assert "02-installation.html" in decision.retry_message
2767
2768
2769 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
2770 temp_dir: Path,
2771 ) -> None:
2772 context = build_context(
2773 temp_dir=temp_dir,
2774 use_react=False,
2775 )
2776 repairer = ResponseRepairer(context)
2777
2778 guide_root = temp_dir / "guides" / "nginx"
2779 chapters = guide_root / "chapters"
2780 chapters.mkdir(parents=True)
2781 index_path = guide_root / "index.html"
2782 chapter_one = chapters / "01-getting-started.html"
2783 chapter_two = chapters / "02-installation.html"
2784 chapter_three = chapters / "03-first-website.html"
2785 chapter_four = chapters / "04-configuration-basics.html"
2786 index_path.write_text("<html></html>\n")
2787 chapter_one.write_text("<h1>One</h1>\n")
2788 chapter_two.write_text("<h1>Two</h1>\n")
2789 chapter_three.write_text("<h1>Three</h1>\n")
2790
2791 implementation_plan = temp_dir / "implementation.md"
2792 implementation_plan.write_text(
2793 "\n".join(
2794 [
2795 "# Implementation Plan",
2796 "",
2797 "## File Changes",
2798 f"- `{guide_root}/`",
2799 f"- `{chapters}/`",
2800 f"- `{index_path}`",
2801 f"- `{chapter_one}`",
2802 f"- `{chapter_two}`",
2803 f"- `{chapter_three}`",
2804 f"- `{chapter_four}`",
2805 "",
2806 ]
2807 )
2808 )
2809
2810 dod = create_definition_of_done("Create a multi-file nginx guide.")
2811 dod.implementation_plan = str(implementation_plan)
2812 dod.touched_files.extend(
2813 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
2814 )
2815 dod.completed_items.extend(
2816 [
2817 "Create the directory structure for the new nginx guide",
2818 "Create the main index.html file with proper structure",
2819 ]
2820 )
2821 dod.pending_items.append("Create each chapter file in sequence")
2822
2823 decision = repairer.handle_empty_response(
2824 task="Create a multi-file nginx guide.",
2825 original_task=None,
2826 empty_retry_count=5,
2827 max_empty_retries=2,
2828 dod=dod,
2829 )
2830
2831 assert decision.should_continue is False
2832 assert decision.final_response is not None
2833 assert "retrying 4 times" in decision.final_response
2834
2835
2836 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
2837 temp_dir: Path,
2838 ) -> None:
2839 context = build_context(
2840 temp_dir=temp_dir,
2841 use_react=False,
2842 )
2843 repairer = ResponseRepairer(context)
2844
2845 guide_root = temp_dir / "guides" / "nginx"
2846 chapters = guide_root / "chapters"
2847 chapters.mkdir(parents=True)
2848 implementation_plan = temp_dir / "implementation.md"
2849 implementation_plan.write_text(
2850 "\n".join(
2851 [
2852 "# Implementation Plan",
2853 "",
2854 "## File Changes",
2855 f"- `{guide_root / 'index.html'}`",
2856 f"- `{chapters / '01-getting-started.html'}`",
2857 f"- `{chapters / '02-installation.html'}`",
2858 "",
2859 ]
2860 )
2861 )
2862
2863 dod = create_definition_of_done("Create a multi-file nginx guide.")
2864 dod.implementation_plan = str(implementation_plan)
2865 dod.touched_files.extend(
2866 [
2867 str(guide_root / "index.html"),
2868 str(chapters / "01-getting-started.html"),
2869 ]
2870 )
2871 dod.completed_items.extend(
2872 [
2873 "Create the directory structure for the new nginx guide",
2874 "Create the main index.html file with proper structure",
2875 ]
2876 )
2877 dod.pending_items.append("Create each chapter file in sequence")
2878
2879 decision = repairer.handle_empty_response(
2880 task="Create a multi-file nginx guide.",
2881 original_task=None,
2882 empty_retry_count=1,
2883 max_empty_retries=2,
2884 dod=dod,
2885 )
2886
2887 assert decision.retry_message is not None
2888 assert "Continue from the exact next step below." in decision.retry_message
2889 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
2890
2891
2892 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
2893 temp_dir: Path,
2894 ) -> None:
2895 context = build_context(
2896 temp_dir=temp_dir,
2897 use_react=False,
2898 )
2899 repairer = ResponseRepairer(context)
2900
2901 guide_root = temp_dir / "guides" / "nginx"
2902 chapters = guide_root / "chapters"
2903 chapters.mkdir(parents=True)
2904 index_path = guide_root / "index.html"
2905 chapter_one = chapters / "01-getting-started.html"
2906 chapter_two = chapters / "02-installation.html"
2907 chapter_three = chapters / "03-first-website.html"
2908 index_path.write_text("<html></html>\n")
2909 chapter_one.write_text("<h1>One</h1>\n")
2910 chapter_two.write_text("<h1>Two</h1>\n")
2911
2912 implementation_plan = temp_dir / "implementation.md"
2913 implementation_plan.write_text(
2914 "\n".join(
2915 [
2916 "# Implementation Plan",
2917 "",
2918 "## File Changes",
2919 f"- `{guide_root}/`",
2920 f"- `{chapters}/`",
2921 f"- `{index_path}`",
2922 f"- `{chapter_one}`",
2923 f"- `{chapter_two}`",
2924 f"- `{chapter_three}`",
2925 "",
2926 ]
2927 )
2928 )
2929
2930 dod = create_definition_of_done("Create a multi-file nginx guide.")
2931 dod.implementation_plan = str(implementation_plan)
2932 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2933 dod.completed_items.extend(
2934 [
2935 "Create the main index.html file with proper structure",
2936 "Link all chapters together properly",
2937 ]
2938 )
2939 dod.pending_items.append("Create each chapter file in sequence")
2940
2941 decision = repairer.handle_empty_response(
2942 task="Create a multi-file nginx guide.",
2943 original_task=None,
2944 empty_retry_count=1,
2945 max_empty_retries=2,
2946 dod=dod,
2947 )
2948
2949 assert decision.retry_message is not None
2950 assert "Link all chapters together properly" not in decision.retry_message
2951 assert "Continue from the exact next step below." in decision.retry_message
2952 assert "Resume with this exact next step:" in decision.retry_message
2953
2954
2955 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
2956 temp_dir: Path,
2957 ) -> None:
2958 context = build_context(
2959 temp_dir=temp_dir,
2960 use_react=False,
2961 )
2962 repairer = ResponseRepairer(context)
2963
2964 reference_chapters = temp_dir / "fortran" / "chapters"
2965 reference_chapters.mkdir(parents=True)
2966 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
2967
2968 guide_root = temp_dir / "guides" / "nginx"
2969 chapters = guide_root / "chapters"
2970 chapters.mkdir(parents=True)
2971 index_path = guide_root / "index.html"
2972 index_path.write_text("<html></html>\n")
2973
2974 implementation_plan = temp_dir / "implementation.md"
2975 implementation_plan.write_text(
2976 "\n".join(
2977 [
2978 "# Implementation Plan",
2979 "",
2980 "## File Changes",
2981 f"- `{guide_root}/`",
2982 f"- `{chapters}/`",
2983 f"- `{index_path}`",
2984 "",
2985 ]
2986 )
2987 )
2988
2989 dod = create_definition_of_done("Create a multi-file nginx guide.")
2990 dod.implementation_plan = str(implementation_plan)
2991 dod.touched_files.append(str(index_path))
2992 dod.pending_items.append("Write the introduction chapter")
2993 context.session.append(
2994 Message(
2995 role=Role.ASSISTANT,
2996 content="",
2997 tool_calls=[
2998 ToolCall(
2999 id="read-ref-1",
3000 name="read",
3001 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
3002 )
3003 ],
3004 )
3005 )
3006
3007 decision = repairer.handle_empty_response(
3008 task="Create a multi-file nginx guide.",
3009 original_task=None,
3010 empty_retry_count=1,
3011 max_empty_retries=2,
3012 dod=dod,
3013 )
3014
3015 assert decision.should_continue is True
3016 assert decision.retry_message is not None
3017 assert (
3018 "Resume with this exact next step: continue `Write the introduction chapter` "
3019 "by creating `01-introduction.html`."
3020 in decision.retry_message
3021 )
3022 assert (
3023 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "01-introduction.html")}", content="...")`.'
3024 in decision.retry_message
3025 )
3026 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
3027 assert display_runtime_path(reference_chapters / "01-introduction.html") not in decision.retry_message