An empty plan against an unmet goal gets one nudge, then is believed - #92
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
An admitted empty proposal ends the run: the planner is saying there is no further work, and the loop believes it. That is the right default — but it held even when the operator's own
goal_reachedcheck said the run was NOT done, and the planner was never told about the contradiction. Observed with qwen3:8b against the stdlib catalog: asked for a security audit, the model returned a valid empty proposal on round 1 and the run stoppedno_further_workhaving spent one planning call and done nothing. The goal check existed, was unsatisfied, and appeared nowhere in any prompt.The empty-proposal branch consulted
goal_reachedonly to choose the label (goal_metvsno_further_work), never to question the stop (loop.py, theif not proposal.nodes:branch).The fix
One nudge, not a counter. When an admitted proposal is empty AND a goal check exists AND it is unsatisfied AND this run has not nudged before, the loop replans once with a note naming the contradiction:
Then the planner's answer is final:
no_further_work, with the detail upgraded to say the planner confirmed it against an unsatisfied goal check.no_further_workwith "the follow-up produced nothing usable" — read as confirmation-by-silence, never counted towardmax_consecutive_planning_failures. This is what keeps the stdlib scripted flow's deliberate honest-clean-stop intact (test_the_scripted_plan_runs_spend_free_and_stops_cleanlyis unchanged and passes).Runs with no goal check, or a satisfied one, behave byte-identically to before: one empty proposal, one immediate clean stop. The existing pins (
test_an_admitted_empty_proposal_ends_the_run_with_no_further_work, stop-reason string set) are untouched and green.Tests
Four new, in
tests/test_planner_loop.py:test_an_empty_proposal_against_an_unmet_goal_gets_one_nudge— empty then real work: run finishesgoal_met, the nudge text appears in round 2's prompt, round 1 is recorded admitted-but-unexecuted.test_a_second_empty_proposal_is_believed— empty twice:no_further_workafter exactly 2 rounds, "confirmed no further work" in the detail, nothing executed.test_an_unusable_reply_after_the_nudge_confirms_no_further_work— script exhausts after the nudge:no_further_work, notplanning_failed.test_an_empty_proposal_with_no_goal_check_stops_without_a_nudge— pins the unchanged default: one round, one call, clean stop.Full suite: 1,991 passed, 12 deselected; ruff clean.
🤖 Generated with Claude Code