tenseleyflow/loader / 36515f8

Browse files

Align missing artifact nudges

Authored by espadonne
SHA
36515f8c873fd12e631592f9682b582822aad0ec
Parents
81934c3
Tree
b80a91d

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 47 0
M tests/test_tool_batches.py 100 0
src/loader/runtime/tool_batches.pymodified
@@ -912,6 +912,17 @@ class ToolBatchRunner:
912912
         )
913913
         if missing_artifact is None:
914914
             return
915
+        next_pending = preferred_pending_todo_item(
916
+            dod,
917
+            project_root=self.context.project_root,
918
+            missing_artifact=missing_artifact,
919
+        )
920
+        missing_artifact = _prefer_missing_artifact_for_pending_item(
921
+            dod,
922
+            missing_artifact=missing_artifact,
923
+            next_pending=next_pending,
924
+            project_root=self.context.project_root,
925
+        )
915926
 
916927
         current_label = _current_mutation_label(tool_call)
917928
         todo_refresh = _todo_refresh_guidance(
@@ -1232,6 +1243,42 @@ def _next_missing_planned_artifact(
12321243
     return None
12331244
 
12341245
 
1246
+def _prefer_missing_artifact_for_pending_item(
1247
+    dod: DefinitionOfDone,
1248
+    *,
1249
+    missing_artifact: tuple[Path, bool] | None,
1250
+    next_pending: str | None,
1251
+    project_root: Path,
1252
+) -> tuple[Path, bool] | None:
1253
+    if missing_artifact is None or not next_pending:
1254
+        return missing_artifact
1255
+
1256
+    inferred_target = infer_pending_todo_output_target(
1257
+        dod,
1258
+        next_pending,
1259
+        project_root=project_root,
1260
+    )
1261
+    if inferred_target is None or inferred_target.exists():
1262
+        return missing_artifact
1263
+
1264
+    normalized_target = inferred_target.expanduser().resolve(strict=False)
1265
+    for planned_target, expect_directory in collect_planned_artifact_targets(
1266
+        dod,
1267
+        project_root=project_root,
1268
+        max_paths=12,
1269
+    ):
1270
+        normalized_planned = planned_target.expanduser().resolve(strict=False)
1271
+        if expect_directory:
1272
+            try:
1273
+                normalized_target.relative_to(normalized_planned)
1274
+            except ValueError:
1275
+                continue
1276
+            return normalized_target, False
1277
+        if normalized_planned == normalized_target:
1278
+            return normalized_target, False
1279
+    return missing_artifact
1280
+
1281
+
12351282
 def _late_stage_missing_artifact_build(
12361283
     dod: DefinitionOfDone,
12371284
     *,
tests/test_tool_batches.pymodified
@@ -1870,6 +1870,106 @@ async def test_tool_batch_runner_observation_handoff_pushes_mutation_step(
18701870
     )
18711871
 
18721872
 
1873
+@pytest.mark.asyncio
1874
+async def test_tool_batch_runner_missing_artifact_nudge_prefers_pending_index_after_mkdir(
1875
+    temp_dir: Path,
1876
+) -> None:
1877
+    async def assess_confidence(
1878
+        tool_name: str,
1879
+        tool_args: dict,
1880
+        context: str,
1881
+    ) -> ConfidenceAssessment:
1882
+        raise AssertionError("Confidence scoring should be disabled in this scenario")
1883
+
1884
+    async def verify_action(
1885
+        tool_name: str,
1886
+        tool_args: dict,
1887
+        result: str,
1888
+        expected: str = "",
1889
+    ) -> ActionVerification:
1890
+        raise AssertionError("Verification should not run for this scenario")
1891
+
1892
+    nginx_root = temp_dir / "Loader" / "guides" / "nginx"
1893
+    chapters = nginx_root / "chapters"
1894
+    implementation_plan = temp_dir / "implementation.md"
1895
+    implementation_plan.write_text(
1896
+        "\n".join(
1897
+            [
1898
+                "# Implementation Plan",
1899
+                "",
1900
+                "## File Changes",
1901
+                f"- `{chapters}/`",
1902
+                f"- `{nginx_root / 'index.html'}`",
1903
+                "",
1904
+            ]
1905
+        )
1906
+    )
1907
+
1908
+    context = build_context(
1909
+        temp_dir=temp_dir,
1910
+        messages=[],
1911
+        safeguards=FakeSafeguards(),
1912
+        assess_confidence=assess_confidence,
1913
+        verify_action=verify_action,
1914
+        auto_recover=False,
1915
+    )
1916
+    queued_messages: list[str] = []
1917
+    context.queue_steering_message_callback = queued_messages.append
1918
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
1919
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
1920
+    dod.implementation_plan = str(implementation_plan)
1921
+    sync_todos_to_definition_of_done(
1922
+        dod,
1923
+        [
1924
+            {
1925
+                "content": "Create the nginx directory structure",
1926
+                "active_form": "Creating the nginx directory structure",
1927
+                "status": "pending",
1928
+            },
1929
+            {
1930
+                "content": "Develop the main index.html file with proper structure",
1931
+                "active_form": "Developing the main index.html file with proper structure",
1932
+                "status": "pending",
1933
+            },
1934
+        ],
1935
+    )
1936
+
1937
+    tool_call = ToolCall(
1938
+        id="mkdir-nginx",
1939
+        name="bash",
1940
+        arguments={"command": f"mkdir -p {chapters}"},
1941
+    )
1942
+    executor = FakeExecutor(
1943
+        [
1944
+            tool_outcome(
1945
+                tool_call=tool_call,
1946
+                output="",
1947
+                is_error=False,
1948
+            )
1949
+        ]
1950
+    )
1951
+
1952
+    summary = TurnSummary(final_response="")
1953
+    await runner.execute_batch(
1954
+        tool_calls=[tool_call],
1955
+        tool_source="assistant",
1956
+        pending_tool_calls_seen=set(),
1957
+        emit=_noop_emit,
1958
+        summary=summary,
1959
+        dod=dod,
1960
+        executor=executor,  # type: ignore[arg-type]
1961
+        on_confirmation=None,
1962
+        on_user_question=None,
1963
+        emit_confirmation=None,
1964
+        consecutive_errors=0,
1965
+    )
1966
+
1967
+    assert queued_messages
1968
+    message = queued_messages[-1]
1969
+    assert "Resume by creating `index.html` now." in message
1970
+    assert "Resume by creating the next output file under `chapters/` now." not in message
1971
+
1972
+
18731973
 @pytest.mark.asyncio
18741974
 async def test_duplicate_observation_nudge_prioritizes_missing_artifact_over_review(
18751975
     temp_dir: Path,