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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user