tenseleyflow/loader / 9097b6f

Browse files

Nudge active quality repairs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
9097b6f61693c22a7d6e059da76ade978d7c743f
Parents
8f1b6b4
Tree
bfe79a7

2 changed files

StatusFile+-
M src/loader/runtime/turn_completion.py 56 0
M tests/test_turn_completion.py 82 0
src/loader/runtime/turn_completion.pymodified
@@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable
66
 from dataclasses import dataclass
77
 from enum import StrEnum
88
 from pathlib import Path
9
+from typing import cast
910
 
1011
 from ..llm.base import Message, Role
1112
 from .completion_policy import CompletionPolicy
@@ -26,6 +27,7 @@ from .policy_timeline import (
2627
     completion_timeline_kind,
2728
 )
2829
 from .repair import ResponseRepairer
30
+from .repair_focus import extract_active_repair_context
2931
 from .rollback import RollbackPlan
3032
 from .verification_observations import VerificationObservation
3133
 from .workflow import (
@@ -416,6 +418,10 @@ def _build_in_progress_continuation(
416418
     if not _looks_like_progress_intent(content):
417419
         return None
418420
 
421
+    quality_repair = _build_html_quality_repair_continuation(messages)
422
+    if quality_repair is not None:
423
+        return quality_repair
424
+
419425
     missing_artifact = _next_missing_planned_artifact(
420426
         dod,
421427
         project_root=project_root,
@@ -472,6 +478,42 @@ def _build_in_progress_continuation(
472478
     return None
473479
 
474480
 
481
+def _build_html_quality_repair_continuation(
482
+    messages: list[object],
483
+) -> InProgressContinuation | None:
484
+    repair = extract_active_repair_context(cast(list[Message], messages))
485
+    if repair is None:
486
+        return None
487
+    if not any(_repair_line_is_html_quality(line) for line in repair.repair_lines):
488
+        return None
489
+
490
+    target_text = repair.artifact_path or (
491
+        repair.allowed_paths[0] if repair.allowed_paths else ""
492
+    )
493
+    if not target_text:
494
+        return None
495
+
496
+    issue_line = next(
497
+        (
498
+            line[2:] if line.startswith("- ") else line
499
+            for line in repair.repair_lines
500
+            if target_text in line and _repair_line_is_html_quality(line)
501
+        ),
502
+        "",
503
+    )
504
+    issue_sentence = f" Current verifier issue: {issue_line}" if issue_line else ""
505
+    prompt = (
506
+        "[CONTINUE QUALITY REPAIR]\n"
507
+        "You just described a content-quality repair, but did not execute it. "
508
+        f"Emit one concrete `patch`, `edit`, or `write` tool call for `{target_text}` now."
509
+        f"{issue_sentence} "
510
+        "Prefer a bounded append or body-section replacement before the existing "
511
+        "back link, footer, or closing body. Do not rewrite the whole file from "
512
+        "memory, do not reopen unrelated reference files, and do not summarize."
513
+    )
514
+    return InProgressContinuation(prompt=prompt, target=None)
515
+
516
+
475517
 def _looks_like_progress_intent(content: str) -> bool:
476518
     text = content.lower().strip()
477519
     if not text or "?" in text:
@@ -481,6 +523,20 @@ def _looks_like_progress_intent(content: str) -> bool:
481523
     return any(marker in text for marker in _PROGRESS_INTENT_HINTS)
482524
 
483525
 
526
+def _repair_line_is_html_quality(line: str) -> bool:
527
+    lowered = line.lower()
528
+    return any(
529
+        phrase in lowered
530
+        for phrase in (
531
+            "thin content",
532
+            "insufficient structured content",
533
+            "content-quality",
534
+            "quality target",
535
+            "html guide content quality",
536
+        )
537
+    )
538
+
539
+
484540
 def _next_missing_planned_artifact(
485541
     dod: DefinitionOfDone,
486542
     *,
tests/test_turn_completion.pymodified
@@ -373,6 +373,88 @@ async def test_turn_completion_interrupts_progress_intent_once_output_files_exis
373373
     )
374374
 
375375
 
376
+@pytest.mark.asyncio
377
+async def test_turn_completion_uses_quality_repair_prompt_for_rewrite_narration(
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="Create an equally thorough HTML guide.",
396
+        emit=capture,
397
+        requested_mode="execute",
398
+        original_task=None,
399
+        on_user_question=None,
400
+    )
401
+    await runtime.phase_tracker.enter(
402
+        TurnPhase.ASSISTANT,
403
+        capture,
404
+        detail="Requesting assistant response",
405
+        reason_code="request_assistant_response",
406
+    )
407
+
408
+    chapter = temp_dir / "guides" / "nginx" / "chapters" / "01-introduction.html"
409
+    chapter.parent.mkdir(parents=True)
410
+    chapter.write_text("<html><body><h1>Intro</h1></body></html>\n")
411
+    prepared.definition_of_done.touched_files.append(str(chapter))
412
+    prepared.definition_of_done.mutating_actions.append("write")
413
+    agent.session.append(
414
+        Message(
415
+            role=Role.USER,
416
+            content=(
417
+                "Repair focus:\n"
418
+                f"- Improve `{chapter}`: insufficient structured content "
419
+                "(12 blocks, expected at least 18).\n"
420
+                f"- Immediate next step: edit `{chapter}` with a substantial "
421
+                "expansion or replacement that satisfies its listed quality issue.\n"
422
+            ),
423
+        )
424
+    )
425
+
426
+    content = (
427
+        "Let me try a different approach by rewriting the entire file with more "
428
+        "comprehensive content:"
429
+    )
430
+    decision = await runtime.turn_completion.handle_text_response(
431
+        content=content,
432
+        response_content=content,
433
+        task=prepared.task,
434
+        effective_task=prepared.effective_task,
435
+        iterations=1,
436
+        max_iterations=agent.config.max_iterations,
437
+        actions_taken=[],
438
+        continuation_count=0,
439
+        dod=prepared.definition_of_done,
440
+        emit=capture,
441
+        summary=prepared.summary,
442
+        executor=prepared.executor,
443
+        rollback_plan=prepared.rollback_plan,
444
+    )
445
+
446
+    assert decision.action == TurnCompletionAction.CONTINUE
447
+    assert prepared.summary.completion_decision_code == "in_progress_transition_continue"
448
+    assert agent.session.messages[-1].role.value == "user"
449
+    assert agent.session.messages[-1].content.startswith("[CONTINUE QUALITY REPAIR]")
450
+    assert str(chapter.resolve(strict=False)) in agent.session.messages[-1].content
451
+    assert (
452
+        "one concrete `patch`, `edit`, or `write` tool call"
453
+        in agent.session.messages[-1].content
454
+    )
455
+    assert "Do not rewrite the whole file from memory" in agent.session.messages[-1].content
456
+
457
+
376458
 @pytest.mark.asyncio
377459
 async def test_turn_completion_allows_first_progress_narration_before_any_output_exists(
378460
     temp_dir: Path,