@@ -285,7 +285,7 @@ async def test_turn_completion_blocks_false_completion_without_preserving_it( |
| 285 | 285 | |
| 286 | 286 | |
| 287 | 287 | @pytest.mark.asyncio |
| 288 | | -async def test_turn_completion_continues_progress_intent_without_dod_gate_spam( |
| 288 | +async def test_turn_completion_interrupts_progress_intent_once_output_files_exist( |
| 289 | 289 | temp_dir: Path, |
| 290 | 290 | ) -> None: |
| 291 | 291 | backend = ScriptedBackend() |
@@ -363,7 +363,9 @@ async def test_turn_completion_continues_progress_intent_without_dod_gate_spam( |
| 363 | 363 | assert decision.continuation_count == 1 |
| 364 | 364 | assert prepared.summary.completion_decision_code == "in_progress_transition_continue" |
| 365 | 365 | assert prepared.summary.assistant_messages[-1].content == content |
| 366 | | - assert agent.session.messages[-1].role.value == "assistant" |
| 366 | + assert agent.session.messages[-1].role.value == "user" |
| 367 | + assert agent.session.messages[-1].content.startswith("[CONTINUE CURRENT STEP]") |
| 368 | + assert "02-installation.html" in agent.session.messages[-1].content |
| 367 | 369 | assert not any( |
| 368 | 370 | message.role.value == "user" |
| 369 | 371 | and message.content.startswith("[PLANNED ARTIFACTS STILL MISSING]") |
@@ -371,6 +373,77 @@ async def test_turn_completion_continues_progress_intent_without_dod_gate_spam( |
| 371 | 373 | ) |
| 372 | 374 | |
| 373 | 375 | |
| 376 | +@pytest.mark.asyncio |
| 377 | +async def test_turn_completion_allows_first_progress_narration_before_any_output_exists( |
| 378 | + temp_dir: Path, |
| 379 | +) -> None: |
| 380 | + backend = ScriptedBackend() |
| 381 | + config = non_streaming_config() |
| 382 | + config.reasoning.completion_check = False |
| 383 | + agent = Agent( |
| 384 | + backend=backend, |
| 385 | + config=config, |
| 386 | + project_root=temp_dir, |
| 387 | + ) |
| 388 | + runtime = ConversationRuntime(agent) |
| 389 | + events = [] |
| 390 | + |
| 391 | + async def capture(event) -> None: |
| 392 | + events.append(event) |
| 393 | + |
| 394 | + prepared = await runtime.turn_preparation.prepare( |
| 395 | + task=( |
| 396 | + "Create a multi-file nginx guide under ~/Loader/guides/nginx " |
| 397 | + "with an index and chapter files." |
| 398 | + ), |
| 399 | + emit=capture, |
| 400 | + requested_mode="execute", |
| 401 | + original_task=None, |
| 402 | + on_user_question=None, |
| 403 | + ) |
| 404 | + await runtime.phase_tracker.enter( |
| 405 | + TurnPhase.ASSISTANT, |
| 406 | + capture, |
| 407 | + detail="Requesting assistant response", |
| 408 | + reason_code="request_assistant_response", |
| 409 | + ) |
| 410 | + |
| 411 | + implementation_plan = temp_dir / "implementation.md" |
| 412 | + implementation_plan.write_text( |
| 413 | + "# Implementation Plan\n\n" |
| 414 | + "## File Changes\n\n" |
| 415 | + f"- `{temp_dir / 'index.html'}`\n" |
| 416 | + f"- `{temp_dir / 'chapters' / '01-introduction.html'}`\n" |
| 417 | + ) |
| 418 | + |
| 419 | + prepared.definition_of_done.implementation_plan = str(implementation_plan) |
| 420 | + prepared.definition_of_done.pending_items.append( |
| 421 | + "Develop the main index.html file for nginx guide" |
| 422 | + ) |
| 423 | + |
| 424 | + content = "Now I'll create the main index.html file for the nginx guide." |
| 425 | + decision = await runtime.turn_completion.handle_text_response( |
| 426 | + content=content, |
| 427 | + response_content=content, |
| 428 | + task=prepared.task, |
| 429 | + effective_task=prepared.effective_task, |
| 430 | + iterations=1, |
| 431 | + max_iterations=agent.config.max_iterations, |
| 432 | + actions_taken=[], |
| 433 | + continuation_count=0, |
| 434 | + dod=prepared.definition_of_done, |
| 435 | + emit=capture, |
| 436 | + summary=prepared.summary, |
| 437 | + executor=prepared.executor, |
| 438 | + rollback_plan=prepared.rollback_plan, |
| 439 | + ) |
| 440 | + |
| 441 | + assert decision.action == TurnCompletionAction.CONTINUE |
| 442 | + assert decision.continuation_count == 1 |
| 443 | + assert prepared.summary.assistant_messages[-1].content == content |
| 444 | + assert agent.session.messages[-1].role.value == "assistant" |
| 445 | + |
| 446 | + |
| 374 | 447 | @pytest.mark.asyncio |
| 375 | 448 | async def test_turn_completion_interrupts_repeated_concrete_progress_narration( |
| 376 | 449 | temp_dir: Path, |