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 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2026-02-16 22:06:33 +01:00
parent 1f60699f38
commit db4cd7d2da
4 changed files with 30 additions and 35 deletions
+19 -30
View File
@@ -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:
+3 -1
View File
@@ -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)
+5 -1
View File
@@ -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:
+3 -3
View File
@@ -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]