Skip to content

Commit 70c6b9e

Browse files
authored
Merge pull request #186 from closedloop-ai/fix/plan-loop-spurious-complete-no-plan
fix(code): treat a missing plan.json at COMPLETE as a spurious completion
2 parents b89c372 + c3a4305 commit 70c6b9e

3 files changed

Lines changed: 140 additions & 1 deletion

File tree

plugins/code/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "code",
33
"description": "Code and planning framework plugin",
4-
"version": "1.14.7",
4+
"version": "1.14.8",
55
"author": {
66
"name": "ClosedLoop",
77
"email": "support@closedloop.ai"

plugins/code/scripts/run-loop.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ fail_loop_user_visible() {
147147
# fail_loop_user_visible.
148148
detect_spurious_complete() {
149149
local workdir="$1"
150+
# Defaults to the global so the existing single-argument call site is
151+
# unchanged; passed explicitly by tests. Non-empty means this run was asked to
152+
# draft a plan from a PRD, which is what makes a missing plan.json a broken
153+
# promise rather than a run that never owed one.
154+
local prd_file="${2-${PRD_FILE:-}}"
150155
local plan_file="$workdir/plan.json"
151156
local state_file="$workdir/state.json"
152157

@@ -166,6 +171,29 @@ detect_spurious_complete() {
166171
fi
167172

168173
if [[ ! -f "$plan_file" ]]; then
174+
# A --prd run exists to produce plan.json. Claiming COMPLETE without one is
175+
# the strongest spurious-completion signal there is, and this branch used to
176+
# wave it through: the checks below only validate pendingTasks INSIDE an
177+
# existing plan, so "no plan at all" -- the case that actually happens --
178+
# was the one case nothing could catch.
179+
#
180+
# Observed: the orchestrator launched plan-draft-writer in the BACKGROUND,
181+
# said "Plan-draft-writer is running in the background. Waiting for
182+
# completion.", and that same turn carried the completion promise. The loop
183+
# ended, the writer was abandoned mid-flight, post-loop code review passed
184+
# vacuously over an empty diff ("the base ref you passed equals HEAD, so
185+
# nothing was examined"), and the run exited 0 having produced nothing. The
186+
# user got an implementation-plan artifact that looked done and was empty.
187+
#
188+
# Scoped to runs that were asked for a plan: a run with no PRD never owed
189+
# one, and is left alone.
190+
if [[ -n "$prd_file" ]]; then
191+
jq -n -c \
192+
--arg subcode "PLAN_MISSING_AT_COMPLETION" \
193+
--arg message "Loop emitted COMPLETE but no plan.json was ever written. The planning phase did not finish -- a background plan-draft-writer that is still running when the completion promise fires is abandoned. Inspect state.json and the loop output, then re-run /code:code to continue." \
194+
'{subcode:$subcode,message:$message}'
195+
return
196+
fi
169197
echo '{}'
170198
return
171199
fi
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
# Tests for detect_spurious_complete() in run-loop.sh.
3+
#
4+
# The case that motivated these: a PLAN run emitted the completion promise while
5+
# plan-draft-writer was still running in the BACKGROUND. The loop ended, the
6+
# writer was abandoned, post-loop code review passed vacuously over an empty
7+
# diff, and the run exited 0 having written no plan.json at all -- so the user's
8+
# implementation-plan artifact looked done and was empty. The guard could not
9+
# see it: its checks only validate pendingTasks INSIDE an existing plan.json.
10+
#
11+
# run-loop.sh guards main() with [[ "${BASH_SOURCE[0]}" == "$0" ]], so sourcing
12+
# it defines the functions without running the loop.
13+
#
14+
# Usage:
15+
# bash plugins/code/scripts/tests/test_spurious_complete.sh
16+
#
17+
# Exit code: 0 if all tests pass, 1 if any test fails.
18+
19+
set -uo pipefail
20+
21+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22+
RUN_LOOP="$SCRIPT_DIR/../run-loop.sh"
23+
24+
PASS_COUNT=0
25+
FAIL_COUNT=0
26+
27+
pass() {
28+
echo " PASS: $1"
29+
PASS_COUNT=$(( PASS_COUNT + 1 ))
30+
}
31+
32+
fail() {
33+
echo " FAIL: $1 -- $2"
34+
FAIL_COUNT=$(( FAIL_COUNT + 1 ))
35+
}
36+
37+
assert_subcode() {
38+
local name="$1" json="$2" expected="$3"
39+
local actual
40+
actual=$(echo "$json" | jq -r '.subcode // ""' 2>/dev/null || echo "")
41+
if [[ "$actual" == "$expected" ]]; then
42+
pass "$name (subcode=$actual)"
43+
else
44+
fail "$name" "expected subcode '$expected', got '$actual' in: $json"
45+
fi
46+
}
47+
48+
assert_not_spurious() {
49+
local name="$1" json="$2"
50+
local actual
51+
actual=$(echo "$json" | jq -r '.subcode // ""' 2>/dev/null || echo "")
52+
if [[ -z "$actual" ]]; then
53+
pass "$name (not flagged)"
54+
else
55+
fail "$name" "expected no subcode, got '$actual' in: $json"
56+
fi
57+
}
58+
59+
# shellcheck source=/dev/null
60+
source "$RUN_LOOP"
61+
62+
echo "detect_spurious_complete"
63+
64+
# --- A --prd run that produced no plan is the reported defect ---------------
65+
WORKDIR=$(mktemp -d)
66+
printf '%s' '{"phase":"Phase 1: Planning","status":"IN_PROGRESS"}' > "$WORKDIR/state.json"
67+
assert_subcode "COMPLETE with no plan.json on a --prd run is spurious" \
68+
"$(detect_spurious_complete "$WORKDIR" "/tmp/prd.md")" \
69+
"PLAN_MISSING_AT_COMPLETION"
70+
rm -rf "$WORKDIR"
71+
72+
# --- A run that never owed a plan is left alone ----------------------------
73+
WORKDIR=$(mktemp -d)
74+
assert_not_spurious "COMPLETE with no plan.json and no PRD is not spurious" \
75+
"$(detect_spurious_complete "$WORKDIR" "")"
76+
rm -rf "$WORKDIR"
77+
78+
# --- The AWAITING_USER hard stop must keep winning -------------------------
79+
# A drafted plan parked for review legitimately has no finished plan yet; the
80+
# new branch must not turn that documented stop into a failure.
81+
WORKDIR=$(mktemp -d)
82+
printf '%s' '{"phase":"Phase 1.1","status":"AWAITING_USER"}' > "$WORKDIR/state.json"
83+
assert_not_spurious "AWAITING_USER outranks the missing-plan check" \
84+
"$(detect_spurious_complete "$WORKDIR" "/tmp/prd.md")"
85+
rm -rf "$WORKDIR"
86+
87+
# --- Existing behaviour: a complete plan is still clean ---------------------
88+
WORKDIR=$(mktemp -d)
89+
printf '%s' '{"pendingTasks":[],"openQuestions":[]}' > "$WORKDIR/plan.json"
90+
assert_not_spurious "a plan with no pending tasks is not spurious" \
91+
"$(detect_spurious_complete "$WORKDIR" "/tmp/prd.md")"
92+
rm -rf "$WORKDIR"
93+
94+
# --- Existing behaviour: pending tasks still flagged, and not as the new code -
95+
WORKDIR=$(mktemp -d)
96+
printf '%s' '{"pendingTasks":[{"id":"T-1.1"}],"openQuestions":[]}' > "$WORKDIR/plan.json"
97+
assert_subcode "pending tasks at completion still flagged" \
98+
"$(detect_spurious_complete "$WORKDIR" "/tmp/prd.md")" \
99+
"PENDING_TASKS_AT_COMPLETION"
100+
rm -rf "$WORKDIR"
101+
102+
WORKDIR=$(mktemp -d)
103+
printf '%s' '{"pendingTasks":[{"id":"T-1.1"}],"openQuestions":[{"id":"Q-1"}]}' > "$WORKDIR/plan.json"
104+
assert_subcode "pending tasks blocked by open questions still flagged" \
105+
"$(detect_spurious_complete "$WORKDIR" "/tmp/prd.md")" \
106+
"PENDING_TASKS_BLOCKED_BY_QUESTIONS"
107+
rm -rf "$WORKDIR"
108+
109+
echo
110+
echo "passed: $PASS_COUNT failed: $FAIL_COUNT"
111+
[[ "$FAIL_COUNT" -eq 0 ]]

0 commit comments

Comments
 (0)