Keep setup nudges on planned roots
- SHA
938dadc9097615763022ae3104d84557df7f7782- Parents
-
6f92132 - Tree
811c0cd
938dadc
938dadc9097615763022ae3104d84557df7f77826f92132
811c0cd| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/tool_batches.py
|
81 | 30 |
| M |
src/loader/runtime/workflow.py
|
20 | 10 |
| M |
tests/test_tool_batches.py
|
19 | 1 |
| M |
tests/test_workflow.py
|
31 | 0 |
src/loader/runtime/tool_batches.pymodified@@ -372,8 +372,10 @@ class ToolBatchRunner: | ||
| 372 | 372 | if next_pending: |
| 373 | 373 | mutation_suffix = "" |
| 374 | 374 | if _todo_is_mutation_step(next_pending): |
| 375 | - mutation_suffix = _missing_artifact_resume_suffix( | |
| 376 | - missing_artifact, | |
| 375 | + mutation_suffix = _pending_item_resume_suffix( | |
| 376 | + dod, | |
| 377 | + next_pending=next_pending, | |
| 378 | + missing_artifact=missing_artifact, | |
| 377 | 379 | project_root=self.context.project_root, |
| 378 | 380 | messages=list(getattr(self.context.session, "messages", []) or []), |
| 379 | 381 | ) |
@@ -840,8 +842,10 @@ class ToolBatchRunner: | ||
| 840 | 842 | |
| 841 | 843 | mutation_suffix = "" |
| 842 | 844 | if _todo_is_mutation_step(next_pending): |
| 843 | - mutation_suffix = _missing_artifact_resume_suffix( | |
| 844 | - missing_artifact, | |
| 845 | + mutation_suffix = _pending_item_resume_suffix( | |
| 846 | + dod, | |
| 847 | + next_pending=next_pending, | |
| 848 | + missing_artifact=missing_artifact, | |
| 845 | 849 | project_root=self.context.project_root, |
| 846 | 850 | messages=list(getattr(self.context.session, "messages", []) or []), |
| 847 | 851 | ) |
@@ -1385,10 +1389,57 @@ def _missing_artifact_resume_suffix( | ||
| 1385 | 1389 | return "" |
| 1386 | 1390 | |
| 1387 | 1391 | target, expect_directory = missing_artifact |
| 1392 | + return _resume_suffix_for_target( | |
| 1393 | + target, | |
| 1394 | + expect_directory=expect_directory, | |
| 1395 | + project_root=project_root, | |
| 1396 | + messages=messages, | |
| 1397 | + ) | |
| 1398 | + | |
| 1399 | + | |
| 1400 | +def _pending_item_resume_suffix( | |
| 1401 | + dod: DefinitionOfDone, | |
| 1402 | + *, | |
| 1403 | + next_pending: str | None, | |
| 1404 | + missing_artifact: tuple[Path, bool] | None, | |
| 1405 | + project_root: Path, | |
| 1406 | + messages: list[Any] | None = None, | |
| 1407 | +) -> str: | |
| 1408 | + if next_pending: | |
| 1409 | + pending_target = infer_pending_todo_output_target( | |
| 1410 | + dod, | |
| 1411 | + next_pending, | |
| 1412 | + project_root=project_root, | |
| 1413 | + ) | |
| 1414 | + if pending_target is not None and not pending_target.exists(): | |
| 1415 | + normalized_target = pending_target.expanduser().resolve(strict=False) | |
| 1416 | + return _resume_suffix_for_target( | |
| 1417 | + normalized_target, | |
| 1418 | + expect_directory=not bool(normalized_target.suffix), | |
| 1419 | + project_root=project_root, | |
| 1420 | + messages=messages, | |
| 1421 | + allow_inferred_child=False, | |
| 1422 | + ) | |
| 1423 | + return _missing_artifact_resume_suffix( | |
| 1424 | + missing_artifact, | |
| 1425 | + project_root=project_root, | |
| 1426 | + messages=messages, | |
| 1427 | + ) | |
| 1428 | + | |
| 1429 | + | |
| 1430 | +def _resume_suffix_for_target( | |
| 1431 | + target: Path, | |
| 1432 | + *, | |
| 1433 | + expect_directory: bool, | |
| 1434 | + project_root: Path, | |
| 1435 | + messages: list[Any] | None = None, | |
| 1436 | + allow_inferred_child: bool = True, | |
| 1437 | +) -> str: | |
| 1388 | 1438 | label = target.name or str(target) |
| 1389 | 1439 | if expect_directory and not label.endswith("/"): |
| 1390 | 1440 | label += "/" |
| 1391 | 1441 | if expect_directory: |
| 1442 | + if allow_inferred_child: | |
| 1392 | 1443 | next_output_file, next_output_source = infer_next_output_file( |
| 1393 | 1444 | target=target, |
| 1394 | 1445 | project_root=project_root, |
src/loader/runtime/workflow.pymodified@@ -927,8 +927,6 @@ def infer_pending_todo_output_target( | ||
| 927 | 927 | project_root=root, |
| 928 | 928 | max_paths=12, |
| 929 | 929 | ) |
| 930 | - | |
| 931 | - if candidates: | |
| 932 | 930 | planned_files = [ |
| 933 | 931 | target |
| 934 | 932 | for target, expect_directory in planned_targets |
@@ -939,6 +937,8 @@ def infer_pending_todo_output_target( | ||
| 939 | 937 | for target, expect_directory in planned_targets |
| 940 | 938 | if expect_directory |
| 941 | 939 | ] |
| 940 | + | |
| 941 | + if candidates: | |
| 942 | 942 | touched_paths = [ |
| 943 | 943 | Path(path) |
| 944 | 944 | for path in dod.touched_files |
@@ -979,6 +979,16 @@ def infer_pending_todo_output_target( | ||
| 979 | 979 | for directory in planned_directories: |
| 980 | 980 | return directory / candidate.name |
| 981 | 981 | |
| 982 | + if target_label and _contains_any(target_label, _BROAD_SETUP_HINTS): | |
| 983 | + for directory in planned_directories: | |
| 984 | + if not planned_artifact_target_satisfied( | |
| 985 | + dod=dod, | |
| 986 | + target=directory, | |
| 987 | + expect_directory=True, | |
| 988 | + project_root=root, | |
| 989 | + ): | |
| 990 | + return directory | |
| 991 | + | |
| 982 | 992 | if not target_label: |
| 983 | 993 | return None |
| 984 | 994 | |
tests/test_tool_batches.pymodified@@ -1122,6 +1122,21 @@ async def test_tool_batch_runner_queues_next_pending_todo_after_discovery_progre | ||
| 1122 | 1122 | reference = temp_dir / "fortran" / "chapters" / "01-introduction.html" |
| 1123 | 1123 | reference.parent.mkdir(parents=True) |
| 1124 | 1124 | reference.write_text("<h1>Introduction</h1>\n<p>Guide cadence.</p>\n") |
| 1125 | + nginx_root = temp_dir / "Loader" / "guides" / "nginx" | |
| 1126 | + chapters = nginx_root / "chapters" | |
| 1127 | + implementation_plan = temp_dir / "implementation.md" | |
| 1128 | + implementation_plan.write_text( | |
| 1129 | + "\n".join( | |
| 1130 | + [ | |
| 1131 | + "# Implementation Plan", | |
| 1132 | + "", | |
| 1133 | + "## File Changes", | |
| 1134 | + f"- `{chapters}/`", | |
| 1135 | + f"- `{nginx_root / 'index.html'}`", | |
| 1136 | + "", | |
| 1137 | + ] | |
| 1138 | + ) | |
| 1139 | + ) | |
| 1125 | 1140 | |
| 1126 | 1141 | context = build_context( |
| 1127 | 1142 | temp_dir=temp_dir, |
@@ -1137,6 +1152,7 @@ async def test_tool_batch_runner_queues_next_pending_todo_after_discovery_progre | ||
| 1137 | 1152 | context.queue_ephemeral_steering_message_callback = ephemeral_messages.append |
| 1138 | 1153 | runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir)) |
| 1139 | 1154 | dod = create_definition_of_done("Create an equally thorough nginx guide.") |
| 1155 | + dod.implementation_plan = str(implementation_plan) | |
| 1140 | 1156 | sync_todos_to_definition_of_done( |
| 1141 | 1157 | dod, |
| 1142 | 1158 | [ |
@@ -1197,9 +1213,11 @@ async def test_tool_batch_runner_queues_next_pending_todo_after_discovery_progre | ||
| 1197 | 1213 | for message in persistent_messages |
| 1198 | 1214 | ) |
| 1199 | 1215 | assert any( |
| 1200 | - "stop gathering more reference material and perform the change now" in message | |
| 1216 | + "Resume by creating `chapters/` now." in message | |
| 1201 | 1217 | for message in persistent_messages |
| 1202 | 1218 | ) |
| 1219 | + assert all("01-introduction.html" not in message for message in persistent_messages) | |
| 1220 | + assert ephemeral_messages == [] | |
| 1203 | 1221 | |
| 1204 | 1222 | |
| 1205 | 1223 | @pytest.mark.asyncio |
tests/test_workflow.pymodified@@ -18,6 +18,7 @@ from loader.runtime.workflow import ( | ||
| 18 | 18 | effective_pending_todo_items, |
| 19 | 19 | enrich_clarify_brief_with_grounding, |
| 20 | 20 | extract_verification_commands_from_markdown, |
| 21 | + infer_pending_todo_output_target, | |
| 21 | 22 | merge_refreshed_todos_with_existing_scope, |
| 22 | 23 | preserve_task_grounded_acceptance_criteria, |
| 23 | 24 | reconcile_aggregate_completion_steps, |
@@ -974,6 +975,36 @@ def test_advance_todos_from_tool_call_tracks_bash_directory_creation_progress() | ||
| 974 | 975 | assert "Create index.html for nginx guide" in dod.pending_items |
| 975 | 976 | |
| 976 | 977 | |
| 978 | +def test_infer_pending_todo_output_target_maps_broad_setup_to_planned_directory( | |
| 979 | + tmp_path: Path, | |
| 980 | +) -> None: | |
| 981 | + dod = create_definition_of_done("Create a multi-file nginx guide.") | |
| 982 | + nginx_root = tmp_path / "Loader" / "guides" / "nginx" | |
| 983 | + chapters = nginx_root / "chapters" | |
| 984 | + implementation_plan = tmp_path / "implementation.md" | |
| 985 | + implementation_plan.write_text( | |
| 986 | + "\n".join( | |
| 987 | + [ | |
| 988 | + "# Implementation Plan", | |
| 989 | + "", | |
| 990 | + "## File Changes", | |
| 991 | + f"- `{chapters}/`", | |
| 992 | + f"- `{nginx_root / 'index.html'}`", | |
| 993 | + "", | |
| 994 | + ] | |
| 995 | + ) | |
| 996 | + ) | |
| 997 | + dod.implementation_plan = str(implementation_plan) | |
| 998 | + | |
| 999 | + target = infer_pending_todo_output_target( | |
| 1000 | + dod, | |
| 1001 | + "Create the nginx directory structure", | |
| 1002 | + project_root=tmp_path, | |
| 1003 | + ) | |
| 1004 | + | |
| 1005 | + assert target == chapters.resolve(strict=False) | |
| 1006 | + | |
| 1007 | + | |
| 977 | 1008 | def test_advance_todos_from_tool_call_does_not_complete_content_study_from_root_index_read() -> None: |
| 978 | 1009 | dod = create_definition_of_done("Create a multi-file nginx guide.") |
| 979 | 1010 | sync_todos_to_definition_of_done( |