Strengthen first-file write retries
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
873539bc38b6b34463c156edf85b5291f9454cd2- Parents
-
9dceae8 - Tree
ee2b0be
873539b
873539bc38b6b34463c156edf85b5291f9454cd29dceae8
ee2b0be| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/repair.py
|
121 | 36 |
| M |
tests/test_repair.py
|
138 | 8 |
src/loader/runtime/repair.pymodified@@ -383,9 +383,13 @@ class ResponseRepairer: | ||
| 383 | 383 | retry_number: int, |
| 384 | 384 | max_empty_retries: int, |
| 385 | 385 | ) -> str | None: |
| 386 | - if not self._has_confirmed_output_file_progress(dod): | |
| 387 | - return None | |
| 388 | - if self._has_confirmed_substantive_output_file_progress(dod): | |
| 386 | + has_confirmed_output_file_progress = self._has_confirmed_output_file_progress( | |
| 387 | + dod | |
| 388 | + ) | |
| 389 | + has_confirmed_substantive_output_file_progress = ( | |
| 390 | + self._has_confirmed_substantive_output_file_progress(dod) | |
| 391 | + ) | |
| 392 | + if has_confirmed_substantive_output_file_progress: | |
| 389 | 393 | return None |
| 390 | 394 | |
| 391 | 395 | next_missing_artifact = self._preferred_resume_missing_artifact(dod) |
@@ -393,6 +397,16 @@ class ResponseRepairer: | ||
| 393 | 397 | dod, |
| 394 | 398 | missing_artifact=next_missing_artifact, |
| 395 | 399 | ) |
| 400 | + if ( | |
| 401 | + next_pending | |
| 402 | + and not _todo_is_mutation_step(next_pending) | |
| 403 | + and not _todo_is_consistency_review_step(next_pending) | |
| 404 | + ): | |
| 405 | + return None | |
| 406 | + if not has_confirmed_output_file_progress and any( | |
| 407 | + str(raw_path).strip() for raw_path in dod.touched_files | |
| 408 | + ): | |
| 409 | + return None | |
| 396 | 410 | inferred_pending_target = ( |
| 397 | 411 | self._infer_pending_item_output_target(dod, next_pending) |
| 398 | 412 | if next_pending |
@@ -412,49 +426,73 @@ class ResponseRepairer: | ||
| 412 | 426 | project_root=self.context.project_root, |
| 413 | 427 | todo_label=next_pending or "", |
| 414 | 428 | ) |
| 415 | - if next_pending and _todo_is_mutation_step(next_pending): | |
| 429 | + if ( | |
| 430 | + next_pending | |
| 431 | + and _todo_is_mutation_step(next_pending) | |
| 432 | + and not todo_describes_broad_setup_step(next_pending) | |
| 433 | + ): | |
| 416 | 434 | first_line = ( |
| 417 | - f"Continue `{next_pending}` by creating `{concrete_target.name}`." | |
| 435 | + "Resume with this exact next step: continue " | |
| 436 | + f"`{next_pending}` by creating `{concrete_target.name}`." | |
| 418 | 437 | ) |
| 419 | 438 | else: |
| 420 | - first_line = f"Create `{concrete_target.name}` now." | |
| 421 | - compact_retry = True | |
| 439 | + first_line = ( | |
| 440 | + f"Resume with this exact next step: create `{concrete_target.name}`." | |
| 441 | + ) | |
| 422 | 442 | |
| 423 | - lines = [ | |
| 443 | + lines: list[str] = [] | |
| 444 | + if ( | |
| 445 | + not has_confirmed_output_file_progress | |
| 446 | + and next_missing_artifact is not None | |
| 447 | + and not next_missing_artifact[1] | |
| 448 | + ): | |
| 449 | + lines.append( | |
| 450 | + "Next missing planned artifact: " | |
| 451 | + f"{self._format_artifact_label(concrete_target, expect_directory=False)}" | |
| 452 | + ) | |
| 453 | + lines.extend( | |
| 454 | + [ | |
| 424 | 455 | first_line, |
| 456 | + "Prefer one `write(content=...)` call for " | |
| 457 | + f"`{display_runtime_path(concrete_target)}` before more research.", | |
| 425 | 458 | self._mutation_tool_scaffold(concrete_target, tool_name="write"), |
| 426 | 459 | ] |
| 460 | + ) | |
| 461 | + if ( | |
| 462 | + next_pending | |
| 463 | + and todo_describes_aggregate_mutation(next_pending) | |
| 464 | + and not todo_describes_broad_setup_step(next_pending) | |
| 465 | + ): | |
| 466 | + lines.insert( | |
| 467 | + 2, | |
| 468 | + f"It is the next concrete output needed to continue `{next_pending}`.", | |
| 469 | + ) | |
| 470 | + if not concrete_target.parent.exists(): | |
| 471 | + lines.append( | |
| 472 | + "The `write` tool can create that file's parent directories automatically, " | |
| 473 | + "so do the write in one step instead of stopping for a separate mkdir." | |
| 474 | + ) | |
| 427 | 475 | if outline_label: |
| 428 | 476 | lines.append( |
| 429 | 477 | f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
| 430 | 478 | ) |
| 431 | - html_scaffold_line = self._known_existing_html_scaffold_line( | |
| 432 | - concrete_target, | |
| 433 | - require_first_substantive_output=True, | |
| 434 | - ) | |
| 435 | - if html_scaffold_line: | |
| 436 | - lines.append(html_scaffold_line) | |
| 437 | - html_starter_line = self._known_html_starter_shape_line( | |
| 438 | - concrete_target, | |
| 439 | - require_first_substantive_output=True, | |
| 440 | - retry_number=retry_number, | |
| 441 | - outline_label=outline_label, | |
| 442 | - ) | |
| 443 | - if html_starter_line: | |
| 444 | - lines.append(html_starter_line) | |
| 445 | - html_payload_line = self._known_minimal_html_payload_line( | |
| 446 | - concrete_target, | |
| 479 | + self._append_concrete_html_write_cues( | |
| 480 | + lines, | |
| 481 | + target=concrete_target, | |
| 447 | 482 | outline_label=outline_label, |
| 448 | 483 | retry_number=retry_number, |
| 484 | + has_confirmed_output_file_progress=has_confirmed_output_file_progress, | |
| 485 | + has_confirmed_substantive_output_file_progress=( | |
| 486 | + has_confirmed_substantive_output_file_progress | |
| 487 | + ), | |
| 449 | 488 | ) |
| 450 | - if html_payload_line: | |
| 451 | - lines.append(html_payload_line) | |
| 452 | 489 | if ( |
| 453 | - not compact_retry | |
| 454 | - and _should_encourage_initial_version( | |
| 490 | + _should_encourage_initial_version( | |
| 455 | 491 | target=concrete_target, |
| 456 | - has_confirmed_output_file_progress=True, | |
| 457 | - has_confirmed_substantive_output_file_progress=False, | |
| 492 | + has_confirmed_output_file_progress=has_confirmed_output_file_progress, | |
| 493 | + has_confirmed_substantive_output_file_progress=( | |
| 494 | + has_confirmed_substantive_output_file_progress | |
| 495 | + ), | |
| 458 | 496 | ) |
| 459 | 497 | ): |
| 460 | 498 | lines.append( |
@@ -930,6 +968,7 @@ class ResponseRepairer: | ||
| 930 | 968 | has_confirmed_output_file_progress |
| 931 | 969 | and not has_confirmed_substantive_output_file_progress |
| 932 | 970 | ), |
| 971 | + allow_initial_concrete_output=False, | |
| 933 | 972 | retry_number=retry_number, |
| 934 | 973 | outline_label=outline_label, |
| 935 | 974 | ) |
@@ -1224,6 +1263,10 @@ class ResponseRepairer: | ||
| 1224 | 1263 | has_confirmed_output_file_progress |
| 1225 | 1264 | and not has_confirmed_substantive_output_file_progress |
| 1226 | 1265 | ) |
| 1266 | + initial_concrete_output = ( | |
| 1267 | + not has_confirmed_output_file_progress | |
| 1268 | + and not has_confirmed_substantive_output_file_progress | |
| 1269 | + ) | |
| 1227 | 1270 | html_scaffold_line = self._known_existing_html_scaffold_line( |
| 1228 | 1271 | target, |
| 1229 | 1272 | require_first_substantive_output=first_substantive_output, |
@@ -1246,6 +1289,7 @@ class ResponseRepairer: | ||
| 1246 | 1289 | and retry_number >= 4 |
| 1247 | 1290 | ) |
| 1248 | 1291 | ), |
| 1292 | + allow_initial_concrete_output=initial_concrete_output, | |
| 1249 | 1293 | retry_number=retry_number, |
| 1250 | 1294 | outline_label=outline_label, |
| 1251 | 1295 | ) |
@@ -1254,6 +1298,7 @@ class ResponseRepairer: | ||
| 1254 | 1298 | html_payload_line = self._known_minimal_html_payload_line( |
| 1255 | 1299 | target, |
| 1256 | 1300 | outline_label=outline_label, |
| 1301 | + allow_initial_concrete_output=initial_concrete_output, | |
| 1257 | 1302 | retry_number=retry_number, |
| 1258 | 1303 | ) |
| 1259 | 1304 | if html_payload_line: |
@@ -1491,14 +1536,27 @@ class ResponseRepairer: | ||
| 1491 | 1536 | target: Path, |
| 1492 | 1537 | *, |
| 1493 | 1538 | require_first_substantive_output: bool, |
| 1539 | + allow_initial_concrete_output: bool, | |
| 1494 | 1540 | retry_number: int, |
| 1495 | 1541 | outline_label: str | None, |
| 1496 | 1542 | ) -> str | None: |
| 1497 | - if not require_first_substantive_output or retry_number < 1: | |
| 1543 | + if ( | |
| 1544 | + not require_first_substantive_output | |
| 1545 | + and not allow_initial_concrete_output | |
| 1546 | + ) or retry_number < 1: | |
| 1498 | 1547 | return None |
| 1499 | 1548 | if target.suffix.lower() not in {".html", ".htm"}: |
| 1500 | 1549 | return None |
| 1501 | - label = outline_label.strip() if outline_label and outline_label.strip() else "this chapter" | |
| 1550 | + label = ( | |
| 1551 | + outline_label.strip() | |
| 1552 | + if outline_label and outline_label.strip() | |
| 1553 | + else self._fallback_html_label(target) | |
| 1554 | + ) | |
| 1555 | + if self._is_index_html_target(target): | |
| 1556 | + return ( | |
| 1557 | + f"If you get stuck, start with `<!DOCTYPE html>`, `<title>{label}</title>`, " | |
| 1558 | + f"`<h1>{label}</h1>`, a short intro paragraph, and a linked chapter list that points at the guide pages you will create under `chapters/`." | |
| 1559 | + ) | |
| 1502 | 1560 | return ( |
| 1503 | 1561 | f"If you get stuck, start with `<title>{label}</title>`, " |
| 1504 | 1562 | f"`<h1>{label}</h1>`, one introductory paragraph, a couple of `<h2>` " |
@@ -1510,6 +1568,7 @@ class ResponseRepairer: | ||
| 1510 | 1568 | target: Path, |
| 1511 | 1569 | *, |
| 1512 | 1570 | outline_label: str | None, |
| 1571 | + allow_initial_concrete_output: bool, | |
| 1513 | 1572 | retry_number: int, |
| 1514 | 1573 | ) -> str | None: |
| 1515 | 1574 | if retry_number < 5: |
@@ -1517,7 +1576,21 @@ class ResponseRepairer: | ||
| 1517 | 1576 | if target.suffix.lower() not in {".html", ".htm"}: |
| 1518 | 1577 | return None |
| 1519 | 1578 | |
| 1520 | - label = outline_label.strip() if outline_label and outline_label.strip() else target.stem | |
| 1579 | + label = ( | |
| 1580 | + outline_label.strip() | |
| 1581 | + if outline_label and outline_label.strip() | |
| 1582 | + else self._fallback_html_label(target) | |
| 1583 | + ) | |
| 1584 | + if self._is_index_html_target(target): | |
| 1585 | + return ( | |
| 1586 | + "If blanking continues, use this minimal starter payload shape inside the `write` call now: " | |
| 1587 | + f"`<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" " | |
| 1588 | + f"content=\"width=device-width, initial-scale=1.0\"><title>{label}</title></head><body>" | |
| 1589 | + f"<div class=\"container\"><h1>{label}</h1><p>...</p><nav><ul>" | |
| 1590 | + "<li><a href=\"chapters/01-...html\">Chapter 1: ...</a></li>" | |
| 1591 | + "<li><a href=\"chapters/02-...html\">Chapter 2: ...</a></li>" | |
| 1592 | + "</ul></nav></div></body></html>` and refine it later." | |
| 1593 | + ) | |
| 1521 | 1594 | return ( |
| 1522 | 1595 | "If blanking continues, use this minimal starter payload shape inside the `write` call now: " |
| 1523 | 1596 | f"`<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" " |
@@ -1527,6 +1600,18 @@ class ResponseRepairer: | ||
| 1527 | 1600 | "</div></body></html>` and refine it later." |
| 1528 | 1601 | ) |
| 1529 | 1602 | |
| 1603 | + @staticmethod | |
| 1604 | + def _is_index_html_target(target: Path) -> bool: | |
| 1605 | + return target.name.lower() in {"index.html", "index.htm"} | |
| 1606 | + | |
| 1607 | + def _fallback_html_label(self, target: Path) -> str: | |
| 1608 | + if self._is_index_html_target(target): | |
| 1609 | + parent_name = target.parent.name.strip() | |
| 1610 | + if parent_name: | |
| 1611 | + return f"{parent_name.title()} Guide" | |
| 1612 | + return "Main Guide" | |
| 1613 | + return target.stem | |
| 1614 | + | |
| 1530 | 1615 | def _best_known_root_html_scaffold(self, target: Path) -> Path | None: |
| 1531 | 1616 | normalized_target = target.expanduser().resolve(strict=False) |
| 1532 | 1617 | if normalized_target.suffix.lower() not in {".html", ".htm"}: |
tests/test_repair.pymodified@@ -315,8 +315,8 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori | ||
| 315 | 315 | in decision.retry_message |
| 316 | 316 | ) |
| 317 | 317 | assert ( |
| 318 | - "Prefer one `write` call for " | |
| 319 | - f"`{display_runtime_path(index_path)}` before any more reference reads." | |
| 318 | + "Prefer one `write(content=...)` call for " | |
| 319 | + f"`{display_runtime_path(index_path)}` before more research." | |
| 320 | 320 | in decision.retry_message |
| 321 | 321 | ) |
| 322 | 322 | assert ( |
@@ -331,7 +331,10 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori | ||
| 331 | 331 | "Write a compact but real initial version of this file now, then refine or expand it in later edits." |
| 332 | 332 | in decision.retry_message |
| 333 | 333 | ) |
| 334 | - assert "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message | |
| 334 | + assert ( | |
| 335 | + "No narration, no TodoWrite, no rereads, and no empty response; emit the mutation tool call now." | |
| 336 | + in decision.retry_message | |
| 337 | + ) | |
| 335 | 338 | |
| 336 | 339 | |
| 337 | 340 | def test_empty_response_retry_uses_directory_creation_for_setup_targets( |
@@ -752,7 +755,11 @@ def test_empty_response_retry_budget_extends_further_after_first_output_file_exi | ||
| 752 | 755 | assert decision.should_continue is True |
| 753 | 756 | assert decision.retry_message is not None |
| 754 | 757 | assert "retry 5/6" in decision.retry_message |
| 755 | - assert "Continue `Create 01-introduction.html` by creating `01-introduction.html`." in decision.retry_message | |
| 758 | + assert ( | |
| 759 | + "Resume with this exact next step: continue `Create 01-introduction.html` " | |
| 760 | + "by creating `01-introduction.html`." | |
| 761 | + in decision.retry_message | |
| 762 | + ) | |
| 756 | 763 | assert 'Emit this tool shape now: `write(file_path="' in decision.retry_message |
| 757 | 764 | assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message |
| 758 | 765 | |
@@ -958,6 +965,125 @@ def test_empty_response_retry_treats_develop_index_step_as_mutation_work( | ||
| 958 | 965 | assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message |
| 959 | 966 | |
| 960 | 967 | |
| 968 | +def test_empty_response_retry_adds_root_html_starter_shape_for_first_index_write( | |
| 969 | + temp_dir: Path, | |
| 970 | +) -> None: | |
| 971 | + context = build_context( | |
| 972 | + temp_dir=temp_dir, | |
| 973 | + use_react=False, | |
| 974 | + ) | |
| 975 | + repairer = ResponseRepairer(context) | |
| 976 | + | |
| 977 | + guide_root = temp_dir / "guides" / "nginx" | |
| 978 | + chapters = guide_root / "chapters" | |
| 979 | + guide_root.mkdir(parents=True) | |
| 980 | + chapters.mkdir() | |
| 981 | + index_path = guide_root / "index.html" | |
| 982 | + chapter_one = chapters / "01-introduction.html" | |
| 983 | + | |
| 984 | + implementation_plan = temp_dir / "implementation.md" | |
| 985 | + implementation_plan.write_text( | |
| 986 | + "\n".join( | |
| 987 | + [ | |
| 988 | + "# Implementation Plan", | |
| 989 | + "", | |
| 990 | + "## File Changes", | |
| 991 | + f"- `{guide_root}/`", | |
| 992 | + f"- `{index_path}`", | |
| 993 | + f"- `{chapters}/`", | |
| 994 | + f"- `{chapter_one}`", | |
| 995 | + "", | |
| 996 | + ] | |
| 997 | + ) | |
| 998 | + ) | |
| 999 | + | |
| 1000 | + dod = create_definition_of_done("Create a multi-file nginx guide.") | |
| 1001 | + dod.implementation_plan = str(implementation_plan) | |
| 1002 | + dod.completed_items.extend( | |
| 1003 | + [ | |
| 1004 | + "First, examine the existing Fortran guide structure to understand the format and depth", | |
| 1005 | + "Create the new nginx guide directory structure", | |
| 1006 | + ] | |
| 1007 | + ) | |
| 1008 | + dod.pending_items.append("Develop the main index.html file with proper structure") | |
| 1009 | + | |
| 1010 | + decision = repairer.handle_empty_response( | |
| 1011 | + task="Create a multi-file nginx guide.", | |
| 1012 | + original_task=None, | |
| 1013 | + empty_retry_count=2, | |
| 1014 | + max_empty_retries=2, | |
| 1015 | + dod=dod, | |
| 1016 | + ) | |
| 1017 | + | |
| 1018 | + assert decision.should_continue is True | |
| 1019 | + assert decision.retry_message is not None | |
| 1020 | + assert ( | |
| 1021 | + "If you get stuck, start with `<!DOCTYPE html>`, `<title>Nginx Guide</title>`, " | |
| 1022 | + "`<h1>Nginx Guide</h1>`, a short intro paragraph, and a linked chapter list that " | |
| 1023 | + "points at the guide pages you will create under `chapters/`." | |
| 1024 | + in decision.retry_message | |
| 1025 | + ) | |
| 1026 | + assert "../index.html" not in decision.retry_message | |
| 1027 | + | |
| 1028 | + | |
| 1029 | +def test_repeated_first_index_retry_includes_root_html_payload_shape( | |
| 1030 | + temp_dir: Path, | |
| 1031 | +) -> None: | |
| 1032 | + context = build_context( | |
| 1033 | + temp_dir=temp_dir, | |
| 1034 | + use_react=False, | |
| 1035 | + ) | |
| 1036 | + repairer = ResponseRepairer(context) | |
| 1037 | + | |
| 1038 | + guide_root = temp_dir / "guides" / "nginx" | |
| 1039 | + chapters = guide_root / "chapters" | |
| 1040 | + guide_root.mkdir(parents=True) | |
| 1041 | + chapters.mkdir() | |
| 1042 | + index_path = guide_root / "index.html" | |
| 1043 | + chapter_one = chapters / "01-introduction.html" | |
| 1044 | + | |
| 1045 | + implementation_plan = temp_dir / "implementation.md" | |
| 1046 | + implementation_plan.write_text( | |
| 1047 | + "\n".join( | |
| 1048 | + [ | |
| 1049 | + "# Implementation Plan", | |
| 1050 | + "", | |
| 1051 | + "## File Changes", | |
| 1052 | + f"- `{guide_root}/`", | |
| 1053 | + f"- `{index_path}`", | |
| 1054 | + f"- `{chapters}/`", | |
| 1055 | + f"- `{chapter_one}`", | |
| 1056 | + "", | |
| 1057 | + ] | |
| 1058 | + ) | |
| 1059 | + ) | |
| 1060 | + | |
| 1061 | + dod = create_definition_of_done("Create a multi-file nginx guide.") | |
| 1062 | + dod.implementation_plan = str(implementation_plan) | |
| 1063 | + dod.completed_items.extend( | |
| 1064 | + [ | |
| 1065 | + "First, examine the existing Fortran guide structure to understand the format and depth", | |
| 1066 | + "Create the new nginx guide directory structure", | |
| 1067 | + ] | |
| 1068 | + ) | |
| 1069 | + dod.pending_items.append("Develop the main index.html file with proper structure") | |
| 1070 | + | |
| 1071 | + decision = repairer.handle_empty_response( | |
| 1072 | + task="Create a multi-file nginx guide.", | |
| 1073 | + original_task=None, | |
| 1074 | + empty_retry_count=5, | |
| 1075 | + max_empty_retries=6, | |
| 1076 | + dod=dod, | |
| 1077 | + ) | |
| 1078 | + | |
| 1079 | + assert decision.should_continue is True | |
| 1080 | + assert decision.retry_message is not None | |
| 1081 | + assert "If blanking continues, use this minimal starter payload shape" in decision.retry_message | |
| 1082 | + assert "<title>Nginx Guide</title>" in decision.retry_message | |
| 1083 | + assert 'href="chapters/01-...html"' in decision.retry_message | |
| 1084 | + assert "../index.html" not in decision.retry_message | |
| 1085 | + | |
| 1086 | + | |
| 961 | 1087 | def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline( |
| 962 | 1088 | temp_dir: Path, |
| 963 | 1089 | ) -> None: |
@@ -1082,7 +1208,8 @@ def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_ | ||
| 1082 | 1208 | assert decision.retry_message is not None |
| 1083 | 1209 | assert "Next missing planned artifact:" not in decision.retry_message |
| 1084 | 1210 | assert ( |
| 1085 | - "Continue `Create chapter files with content and structure` by creating `01-introduction.html`." | |
| 1211 | + "Resume with this exact next step: continue `Create chapter files with content and structure` " | |
| 1212 | + "by creating `01-introduction.html`." | |
| 1086 | 1213 | in decision.retry_message |
| 1087 | 1214 | ) |
| 1088 | 1215 | assert ( |
@@ -1636,7 +1763,8 @@ def test_empty_response_retry_prefers_output_index_over_reference_index_with_sam | ||
| 1636 | 1763 | assert decision.should_continue is True |
| 1637 | 1764 | assert decision.retry_message is not None |
| 1638 | 1765 | assert ( |
| 1639 | - f"Continue `Develop the nginx index.html file` by creating `{output_index.name}`." | |
| 1766 | + "Resume with this exact next step: continue `Develop the nginx index.html file` " | |
| 1767 | + f"by creating `{output_index.name}`." | |
| 1640 | 1768 | in decision.retry_message |
| 1641 | 1769 | ) |
| 1642 | 1770 | assert ( |
@@ -1703,7 +1831,8 @@ def test_empty_response_retry_points_at_declared_child_file_within_incomplete_ou | ||
| 1703 | 1831 | assert decision.should_continue is True |
| 1704 | 1832 | assert decision.retry_message is not None |
| 1705 | 1833 | assert ( |
| 1706 | - "Continue `Write the introduction chapter` by creating `introduction.html`." | |
| 1834 | + "Resume with this exact next step: continue `Write the introduction chapter` " | |
| 1835 | + "by creating `introduction.html`." | |
| 1707 | 1836 | in decision.retry_message |
| 1708 | 1837 | ) |
| 1709 | 1838 | assert "Next declared output under `chapters/`" not in decision.retry_message |
@@ -2411,7 +2540,8 @@ def test_empty_response_retry_names_next_file_from_observed_sibling_directory( | ||
| 2411 | 2540 | assert decision.should_continue is True |
| 2412 | 2541 | assert decision.retry_message is not None |
| 2413 | 2542 | assert ( |
| 2414 | - "Continue `Write the introduction chapter` by creating `01-introduction.html`." | |
| 2543 | + "Resume with this exact next step: continue `Write the introduction chapter` " | |
| 2544 | + "by creating `01-introduction.html`." | |
| 2415 | 2545 | in decision.retry_message |
| 2416 | 2546 | ) |
| 2417 | 2547 | assert ( |