fix(issue-25): one plan-text check for every load; an empty planner report stops the loop, an empty refine keeps the plan

The --plan-file check landed on the one path a human is watching. The three
--plan-mode loads read a report written by another agent with nobody watching,
and had no content check at all: find_report only tests that the path exists.

An empty or whitespace-only report therefore blanked current_plan in silence.
Phase 2's `if self.current_plan` then flipped the prompt to self-planning and
the run finished rc 0, having paid for a planner job, a Creator challenge and a
refine job and thrown the result away. The escalation refine was worse: it has
no guard, so the correction prompt asserted "follow the updated plan below" and
carried nothing.

All four loads share _plan_text() now - a plan is a readable regular file with
text in it - and each caller says what None means. An empty initial plan stops
the run instead of demoting it, and an empty refine warns and keeps the plan it
already has, which is still good. Those two together leave the escalation
prompt unable to claim a plan it does not carry, so it needs no new branch.

Found by the Creator's challenge to the plan (job 0f63ac1d), sustained by the
Planner as revision 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-21 03:26:34 +00:00
1 parent a28711cccf
commit e551e3baf1
1 file changed
+22 -5
+22 -5
View File
@@ -1017,7 +1017,13 @@ class LoopRunner:
if not p_file or not p_file.exists(): if not p_file or not p_file.exists():
log_error("Final plan file not found.") log_error("Final plan file not found.")
return 1 return 1
self.current_plan = p_file.read_text(encoding="utf-8", errors="replace") plan_text = self._plan_text(p_file)
if plan_text is None:
# D13: the run paid for a planning phase and got nothing back.
# Falling through would silently demote it to self-planning.
log_error(f"the planner's report has no plan text: {p_file}")
return 1
self.current_plan = plan_text
# Interactive turns # Interactive turns
creator_type = self.resolve_agent_type(self.creator, state) creator_type = self.resolve_agent_type(self.creator, state)
@@ -1040,8 +1046,13 @@ class LoopRunner:
return 1 return 1
ref_file = self.find_report(ref_jid) ref_file = self.find_report(ref_jid)
if ref_file and ref_file.exists(): refined = self._plan_text(ref_file) if ref_file else None
self.current_plan = ref_file.read_text(encoding="utf-8", errors="replace") if refined is None:
# D14: overwriting with an empty report loses a plan that is
# still good, and Phase 2 would quietly switch prompts.
log_warn(f"the refine report has no plan text (job {ref_jid}); keeping the previous plan.")
else:
self.current_plan = refined
log_success("Interactive planning completed. Plan finalized.") log_success("Interactive planning completed. Plan finalized.")
else: else:
@@ -1265,8 +1276,14 @@ class LoopRunner:
log_error("Failed to register plan refinement job.") log_error("Failed to register plan refinement job.")
return 1 return 1
ref_file = self.find_report(ref_jid) ref_file = self.find_report(ref_jid)
if ref_file and ref_file.exists(): refined = self._plan_text(ref_file) if ref_file else None
self.current_plan = ref_file.read_text(encoding="utf-8", errors="replace") if refined is None:
log_warn(f"the refine report has no plan text (job {ref_jid}); keeping the previous plan.")
else:
self.current_plan = refined
# D13 stops a plan-mode run whose initial plan is empty and D14
# never overwrites with an empty one, so current_plan is not
# empty here: this prompt cannot claim a plan it does not carry.
correction_prompt = f"갱신된 다음 계획서에 입각하여 지적된 오류들을 수정하고 코드를 다시 구현해주세요. 계획서:\n{self.current_plan}\n피드백 상세:\n{feedback_aggregate}" correction_prompt = f"갱신된 다음 계획서에 입각하여 지적된 오류들을 수정하고 코드를 다시 구현해주세요. 계획서:\n{self.current_plan}\n피드백 상세:\n{feedback_aggregate}"
else: else:
log_info("Applying straight bugfixes based on reviewer feedback...") log_info("Applying straight bugfixes based on reviewer feedback...")