From db4cd7d2da346fddb0077ee02fd7b85828ab3abf Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Mon, 16 Feb 2026 22:06:33 +0100 Subject: [PATCH] fix: extract stuck-subtask loader, use actual_output for stuck notes - Extract duplicated stuck-subtask loading into _load_stuck_subtask_ids() - Write stuck reason to actual_output instead of notes field for consistency with the QA reviewer's expectations - Clarify progress message to mention terminal states (completed/failed/stuck) - Update test assertions to match actual_output field Co-Authored-By: Claude Opus 4.6 --- apps/backend/core/progress.py | 49 ++++++++++++------------------- apps/backend/qa/loop.py | 4 ++- apps/backend/services/recovery.py | 6 +++- tests/test_recovery.py | 6 ++-- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/apps/backend/core/progress.py b/apps/backend/core/progress.py index 68571451..7f69be72 100644 --- a/apps/backend/core/progress.py +++ b/apps/backend/core/progress.py @@ -115,6 +115,23 @@ def is_build_complete(spec_dir: Path) -> bool: return total > 0 and completed == total +def _load_stuck_subtask_ids(spec_dir: Path) -> set[str]: + """Load IDs of subtasks marked as stuck from attempt_history.json.""" + stuck_subtask_ids: set[str] = set() + attempt_history_file = spec_dir / "memory" / "attempt_history.json" + if attempt_history_file.exists(): + try: + with open(attempt_history_file, encoding="utf-8") as f: + attempt_history = json.load(f) + for entry in attempt_history.get("stuck_subtasks", []): + if "subtask_id" in entry: + stuck_subtask_ids.add(entry["subtask_id"]) + except (OSError, json.JSONDecodeError, UnicodeDecodeError): + # Corrupted attempt history is non-fatal; skip stuck-subtask filtering + pass + return stuck_subtask_ids + + def is_build_ready_for_qa(spec_dir: Path) -> bool: """ Check if the build is ready for QA validation. @@ -133,20 +150,7 @@ def is_build_ready_for_qa(spec_dir: Path) -> bool: if not plan_file.exists(): return False - # Load stuck subtask IDs from attempt_history.json - stuck_subtask_ids = set() - attempt_history_file = spec_dir / "memory" / "attempt_history.json" - if attempt_history_file.exists(): - try: - with open(attempt_history_file, encoding="utf-8") as f: - attempt_history = json.load(f) - stuck_subtask_ids = { - entry["subtask_id"] - for entry in attempt_history.get("stuck_subtasks", []) - if "subtask_id" in entry - } - except (OSError, json.JSONDecodeError, UnicodeDecodeError): - pass + stuck_subtask_ids = _load_stuck_subtask_ids(spec_dir) try: with open(plan_file, encoding="utf-8") as f: @@ -475,22 +479,7 @@ def get_next_subtask(spec_dir: Path) -> dict | None: if not plan_file.exists(): return None - # Load stuck subtasks from recovery manager's attempt history - stuck_subtask_ids = set() - attempt_history_file = spec_dir / "memory" / "attempt_history.json" - if attempt_history_file.exists(): - try: - with open(attempt_history_file, encoding="utf-8") as f: - attempt_history = json.load(f) - # Collect IDs of subtasks marked as stuck - stuck_subtask_ids = { - entry["subtask_id"] - for entry in attempt_history.get("stuck_subtasks", []) - if "subtask_id" in entry - } - except (OSError, json.JSONDecodeError, UnicodeDecodeError): - # If we can't read the file, continue without stuck checking - pass + stuck_subtask_ids = _load_stuck_subtask_ids(spec_dir) try: with open(plan_file, encoding="utf-8") as f: diff --git a/apps/backend/qa/loop.py b/apps/backend/qa/loop.py index c8ad68a8..9bf7f5d7 100644 --- a/apps/backend/qa/loop.py +++ b/apps/backend/qa/loop.py @@ -129,7 +129,9 @@ async def run_qa_validation_loop( print("\n❌ Build is not ready for QA validation.") completed, total = count_subtasks(spec_dir) debug("qa_loop", "Build progress", completed=completed, total=total) - print(f" Progress: {completed}/{total} subtasks completed") + print( + f" Progress: {completed}/{total} subtasks in terminal state (completed/failed/stuck)" + ) return False # Emit phase event at start of QA validation (before any early returns) diff --git a/apps/backend/services/recovery.py b/apps/backend/services/recovery.py index 2e9b5898..ea153b59 100644 --- a/apps/backend/services/recovery.py +++ b/apps/backend/services/recovery.py @@ -527,7 +527,11 @@ class RecoveryManager: for subtask in phase.get("subtasks", []): if subtask.get("id") == subtask_id: subtask["status"] = "failed" - subtask["notes"] = f"Marked as stuck: {reason}" + stuck_note = f"Marked as stuck: {reason}" + existing = subtask.get("actual_output", "") + subtask["actual_output"] = ( + f"{stuck_note}\n{existing}" if existing else stuck_note + ) updated = True break if updated: diff --git a/tests/test_recovery.py b/tests/test_recovery.py index d63cfd88..cd40e432 100755 --- a/tests/test_recovery.py +++ b/tests/test_recovery.py @@ -298,9 +298,9 @@ def test_mark_subtask_stuck_updates_plan(test_env): subtask_1_1 = updated_plan["phases"][0]["subtasks"][0] assert subtask_1_1["id"] == "subtask-1-1" assert subtask_1_1["status"] == "failed", "Stuck subtask status should be 'failed'" - assert "notes" in subtask_1_1, "Notes field should be added" - assert "Marked as stuck" in subtask_1_1["notes"], "Notes should mention stuck status" - assert reason in subtask_1_1["notes"], "Notes should include the reason" + assert "actual_output" in subtask_1_1, "actual_output field should be added" + assert "Marked as stuck" in subtask_1_1["actual_output"], "actual_output should mention stuck status" + assert reason in subtask_1_1["actual_output"], "actual_output should include the reason" # Verify other subtask was not affected subtask_1_2 = updated_plan["phases"][0]["subtasks"][1]