Skip to content

fix(code): treat a missing plan.json at COMPLETE as a spurious completion - #186

Merged
mikeangstadt merged 1 commit into
mainfrom
fix/plan-loop-spurious-complete-no-plan
Aug 11, 2026
Merged

fix(code): treat a missing plan.json at COMPLETE as a spurious completion#186
mikeangstadt merged 1 commit into
mainfrom
fix/plan-loop-spurious-complete-no-plan

Conversation

@mikeangstadt

Copy link
Copy Markdown
Collaborator

Summary

Generate Implementation Plan silently produces nothing. The run completes, exits 0, and the user is left with an implementation-plan artifact that looks done and is empty.

Reproduced 2026-08-10 (symphony-alpha PLN-1688). From the loop's own output, in order:

No plan-source.md found. Launching plan-draft-writer to create the plan.
Plan-draft-writer is running in the background. Waiting for completion.
...
Completion promise detected: COMPLETE
Loop complete after 1 iterations!
→ Post-Loop Code Review

The orchestrator launched plan-draft-writer as a background agent, said in plain text that it was waiting for it, and that same turn carried the completion promise. The loop ended, the writer was abandoned mid-flight, and the post-loop code review then passed vacuously over an empty diff — "No findings — but this is a vacuous pass... The base ref you passed equals HEAD, so nothing was examined" — and the process exited 0.

Corroborating state left in the worktree: state.json still reads {"phase":"Phase 1: Planning","status":"IN_PROGRESS"} while runs.log records plan_executereview_approve. Pre-exploration did finish (code-map.json 17.6K, investigation-log.md 16.4K, requirements-extract.json 14.1K) — the run stopped exactly at the plan-writing step.

detect_spurious_complete had the hole backwards. Its checks validate pendingTasks inside an existing plan.json, and its first branch returned "not spurious" whenever the file was absent:

if [[ ! -f "$plan_file" ]]; then
  echo '{}'   # <- let COMPLETE through
  return
fi

So "no plan at all" — the case that actually happens — was the one case nothing could catch. A --prd run exists to produce plan.json; claiming COMPLETE without one is the strongest spurious-completion signal there is.

Change

  • A missing plan.json at COMPLETE is now flagged as PLAN_MISSING_AT_COMPLETION, routed through the existing handle_spurious_complete path (telemetry, lock release, user-visible failure, exit 1) — so it bails before the post-loop code review rather than compounding misleading state, exactly as the sibling subcodes do.
  • Scoped: only fires when the run was asked for a plan (--prd / PRD_FILE). A run with no PRD never owed a plan and is untouched.
  • The AWAITING_USER hard stop still outranks it, so a plan drafted and parked at the Phase 1.1 review checkpoint is not turned into a failure.
  • detect_spurious_complete takes the PRD path as an optional second argument defaulting to the global, so the existing call site is unchanged and tests can be explicit.
  • plugins/code version bumped 1.14.7 → 1.14.8 per the Plugin Version Bump gate.

Test plan

New plugins/code/scripts/tests/test_spurious_complete.sh (6 cases). run-loop.sh guards main with [[ "${BASH_SOURCE[0]}" == "$0" ]], so the suite sources it and calls the function directly:

  • COMPLETE with no plan.json on a --prd run → PLAN_MISSING_AT_COMPLETION (the reported defect)
  • COMPLETE with no plan.json and no PRD → not flagged (scope guard)
  • AWAITING_USER + no plan → not flagged (the documented hard stop still wins)
  • a plan with no pending tasks → not flagged
  • pending tasks → still PENDING_TASKS_AT_COMPLETION
  • pending tasks + open questions → still PENDING_TASKS_BLOCKED_BY_QUESTIONS
passed: 6  failed: 0

Counterfactual: neutering only the new branch fails exactly the first case and leaves the other five green — so the new code is doing the work, and the four pre-existing behaviours are provably unchanged.

Known gap: CI runs ruff/pyright/pytest and the TS suite; there is no bash-test job, so this suite (like the existing plugins/code/hooks/tests/*.sh) is not gated. Run it with bash plugins/code/scripts/tests/test_spurious_complete.sh. Wiring the bash suites into CI is worth doing separately.

Related

The other half of this failure is in closedloop-ai/symphony-alpha (ISS-5872, URGENT): the desktop loop harness computes missingRequired via validateResultBundle, logs Missing required artifacts for PLAN: plan.json, then discards the result and finalizes the loop as COMPLETED with error: null. Either layer alone would have caught this; both failed open. This PR closes the upstream one.

…tion

A PLAN run emitted the completion promise while plan-draft-writer was
still running in the BACKGROUND. The loop ended, the writer was abandoned
mid-flight, post-loop code review passed vacuously over an empty diff
("the base ref you passed equals HEAD, so nothing was examined"), and the
run exited 0 having produced no plan at all. Downstream, the user's
implementation-plan artifact looked done and was empty.

detect_spurious_complete could not see it. Its checks validate pendingTasks
INSIDE an existing plan.json, and its first branch returned "not spurious"
whenever the file was absent -- so "no plan at all", the case that actually
happens, was the one case nothing could catch.

A --prd run exists to produce plan.json, so claiming COMPLETE without one
is now flagged as PLAN_MISSING_AT_COMPLETION. Scoped to runs that were
asked for a plan; a run with no PRD never owed one and is untouched. The
AWAITING_USER hard stop still outranks the new branch, so a drafted plan
parked for review is not turned into a failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikeangstadt
mikeangstadt merged commit 70c6b9e into main Aug 11, 2026
5 checks passed
@mikeangstadt
mikeangstadt deleted the fix/plan-loop-spurious-complete-no-plan branch August 11, 2026 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant