From 75e2f578463e6a440146de94248ffc539a9d1d38 Mon Sep 17 00:00:00 2001 From: "wentao.zhao" Date: Tue, 16 Jun 2026 15:14:45 +0800 Subject: [PATCH 1/4] Block RLCR placeholder summaries Co-Authored-By: Claude Opus 4.8 (1M context) --- hooks/loop-codex-stop-hook.sh | 44 ++++ .../block/work-summary-placeholder.md | 21 ++ prompt-template/claude/drift-replan-prompt.md | 6 + prompt-template/claude/next-round-prompt.md | 6 + tests/test-summary-placeholder-gate.sh | 211 ++++++++++++++++++ 5 files changed, 288 insertions(+) create mode 100644 prompt-template/block/work-summary-placeholder.md create mode 100755 tests/test-summary-placeholder-gate.sh diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 0c191d4c..1ac5fbe9 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -821,6 +821,50 @@ Please write your work summary to: {{SUMMARY_FILE}}" exit 0 fi +# Check Summary File Is Not Still the Scaffold +# ============================================ +# Round summary files are pre-created as editing targets. A file existing is +# therefore not proof that Claude actually summarized completed work. Block the +# common contract-only failure mode before spending a Codex review when the +# summary still contains template placeholders. +if [[ "$IS_FINALIZE_PHASE" != "true" ]]; then + SUMMARY_PLACEHOLDERS=$(awk ' + BEGIN { in_fence = 0 } + /^[[:space:]]*(```|~~~)/ { in_fence = !in_fence; next } + in_fence { next } + /^[[:space:]]*(-[[:space:]]*)?\[(Describe what was (done|implemented in this phase)|List (files created\/modified\/deleted|created\/modified files|tests\/commands run and outcomes|any deferred or pending items|unresolved items, if any))\][[:space:]]*$/ { print FNR ":" $0; next } + /^[[:space:]]*(-[[:space:]]*)?Action:[[:space:]]*none\|add\|update[[:space:]]*$/ { print FNR ":" $0; next } + /^[[:space:]]*(-[[:space:]]*)?Notes:[[:space:]]*\[what changed and why\][[:space:]]*$/ { print FNR ":" $0; next } + ' "$SUMMARY_FILE" 2>/dev/null || true) + if [[ -n "$SUMMARY_PLACEHOLDERS" ]]; then + FALLBACK="# Work Summary Still Placeholder + +The summary file exists but still contains scaffold placeholder text: + +{{PLACEHOLDER_LINES}} + +Writing the round contract is only step 0; it is not implementation progress. +Before exiting, complete at least one non-queued mainline/blocking task and +replace the summary with concrete work, changed files, validation, and remaining +items. + +Summary file: {{SUMMARY_FILE}}" + REASON=$(load_and_render_safe "$TEMPLATE_DIR" "block/work-summary-placeholder.md" "$FALLBACK" \ + "SUMMARY_FILE=$SUMMARY_FILE" \ + "PLACEHOLDER_LINES=$SUMMARY_PLACEHOLDERS") + + jq -n \ + --arg reason "$REASON" \ + --arg msg "Loop: Summary file still contains placeholders for round $CURRENT_ROUND" \ + '{ + "decision": "block", + "reason": $reason, + "systemMessage": $msg + }' + exit 0 + fi +fi + # Check Round Contract Exists # ======================================== diff --git a/prompt-template/block/work-summary-placeholder.md b/prompt-template/block/work-summary-placeholder.md new file mode 100644 index 00000000..129c750d --- /dev/null +++ b/prompt-template/block/work-summary-placeholder.md @@ -0,0 +1,21 @@ +# Work Summary Still Placeholder + +You attempted to exit while the round summary still contains scaffold placeholder text. + +**Placeholder lines detected:** + +```text +{{PLACEHOLDER_LINES}} +``` + +**Required Action**: +1. Do not treat the round contract as implementation progress; it is only step 0. +2. Complete at least one non-queued `[mainline]` or truly `[blocking]` task. +3. Replace the summary at `{{SUMMARY_FILE}}` with concrete evidence: + - What was implemented + - Files created/modified + - Validation/tests/commands and outcomes + - Remaining items, if any + - BitLesson Delta + +After replacing the placeholder summary, retry the exit. diff --git a/prompt-template/claude/drift-replan-prompt.md b/prompt-template/claude/drift-replan-prompt.md index a5970c59..8b13b699 100644 --- a/prompt-template/claude/drift-replan-prompt.md +++ b/prompt-template/claude/drift-replan-prompt.md @@ -32,6 +32,12 @@ Your recovery contract must contain: Do not start implementation until the recovery contract exists. +**Important**: Rewriting the recovery contract is only the re-anchor +precondition for drift recovery; it is not implementation progress. Do not +attempt to exit after contract-only work. Before exiting, complete at least one +non-queued `[mainline]` or truly `[blocking]` task that proves recovered +mainline movement and replace the summary scaffold with concrete evidence. + ## Task Lane Rules Use the Task system (TaskCreate, TaskUpdate, TaskList) with one required tag per task: diff --git a/prompt-template/claude/next-round-prompt.md b/prompt-template/claude/next-round-prompt.md index fd1b1cfe..a47c9474 100644 --- a/prompt-template/claude/next-round-prompt.md +++ b/prompt-template/claude/next-round-prompt.md @@ -26,6 +26,12 @@ Your round contract must contain: Do not start implementation until the round contract exists. +**Important**: Writing or updating the round contract is only the re-anchor +precondition for this round; it is not implementation progress. Do not attempt +to exit after contract-only work. Before exiting, complete at least one +non-queued `[mainline]` or truly `[blocking]` task and replace the summary +scaffold with concrete evidence. + ## Task Lane Rules Use the Task system (TaskCreate, TaskUpdate, TaskList) with one required tag per task: diff --git a/tests/test-summary-placeholder-gate.sh b/tests/test-summary-placeholder-gate.sh new file mode 100755 index 00000000..1e305f20 --- /dev/null +++ b/tests/test-summary-placeholder-gate.sh @@ -0,0 +1,211 @@ +#!/usr/bin/env bash +# +# Regression tests for summary placeholder gating in loop-codex-stop-hook.sh. +# +# Round summary files are pre-created as scaffold targets. The stop hook must +# reject a scaffold summary before running Codex, otherwise a contract-only +# round can be reviewed as if implementation work was attempted. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$SCRIPT_DIR/test-helpers.sh" + +STOP_HOOK="$PROJECT_ROOT/hooks/loop-codex-stop-hook.sh" + +setup_test_dir +export XDG_CACHE_HOME="$TEST_DIR/.cache" +mkdir -p "$XDG_CACHE_HOME" + +setup_mock_codex() { + mkdir -p "$TEST_DIR/bin" + cat > "$TEST_DIR/bin/codex" << 'EOF' +#!/usr/bin/env bash +if [[ -n "${MOCK_CODEX_MARKER:-}" ]]; then + : > "$MOCK_CODEX_MARKER" +fi +echo "Mainline Progress Verdict: STALLED" +echo "Final Decision: NOT COMPLETE" +exit 0 +EOF + chmod +x "$TEST_DIR/bin/codex" + export PATH="$TEST_DIR/bin:$PATH" +} + +create_loop_fixture() { + local repo_dir="$1" + local summary_body="$2" + + init_test_git_repo "$repo_dir" + printf 'plans/\n' > "$repo_dir/.gitignore" + git -C "$repo_dir" add .gitignore + git -C "$repo_dir" commit -q -m "Add gitignore" + + mkdir -p "$repo_dir/plans" + cat > "$repo_dir/plans/plan.md" << 'EOF' +# Test Plan + +Complete real implementation work. +EOF + + local loop_dir="$repo_dir/.humanize/rlcr/2026-03-01_00-00-00" + mkdir -p "$loop_dir" + cp "$repo_dir/plans/plan.md" "$loop_dir/plan.md" + + local branch base_commit + branch=$(git -C "$repo_dir" rev-parse --abbrev-ref HEAD) + base_commit=$(git -C "$repo_dir" rev-parse HEAD) + + cat > "$loop_dir/state.md" << EOF +--- +current_round: 0 +max_iterations: 42 +codex_model: gpt-5.5 +codex_effort: high +codex_timeout: 60 +push_every_round: false +full_review_round: 5 +plan_file: "plans/plan.md" +plan_tracked: false +start_branch: $branch +base_branch: $branch +base_commit: $base_commit +review_started: false +ask_codex_question: false +agent_teams: false +bitlesson_required: false +mainline_stall_count: 0 +last_mainline_verdict: unknown +drift_status: normal +--- +EOF + + printf '%s\n' "$summary_body" > "$loop_dir/round-0-summary.md" + + cat > "$loop_dir/round-0-contract.md" << 'EOF' +# Round 0 Contract + +- Mainline Objective: Complete real implementation work. +- Target ACs: AC-1. +- Blocking: none. +- Queued: none. +- Success Criteria: concrete implementation evidence exists. +EOF + + cat > "$loop_dir/goal-tracker.md" << 'EOF' +# Goal Tracker +## IMMUTABLE SECTION +### Ultimate Goal +Complete real implementation work. +### Acceptance Criteria +- AC-1: Work evidence exists. +--- +## MUTABLE SECTION +### Plan Version: 1 (Updated: Round 0) +#### Active Tasks +| Task | Target AC | Status | Tag | Owner | Notes | +|------|-----------|--------|-----|-------|-------| +EOF + + echo "$loop_dir" +} + +run_stop_hook() { + local repo_dir="$1" + printf '{"hook_event_name":"Stop","cwd":"%s","session_id":"test-session","transcript_path":null}\n' "$repo_dir" \ + | CLAUDE_PROJECT_DIR="$repo_dir" "$STOP_HOOK" +} + +setup_mock_codex + +echo "==========================================" +echo "Summary Placeholder Gate Tests" +echo "==========================================" + +# Test 1: scaffold summary blocks before Codex runs. +repo1="$TEST_DIR/repo-placeholder" +marker1="$TEST_DIR/codex-ran-placeholder" +summary_placeholder='# Round 0 Summary + +## Work Completed +- [Describe what was implemented in this phase] + +## Files Changed +- [List created/modified files] + +## Validation +- [List tests/commands run and outcomes]' +create_loop_fixture "$repo1" "$summary_placeholder" >/dev/null +export MOCK_CODEX_MARKER="$marker1" +output1=$(run_stop_hook "$repo1") +unset MOCK_CODEX_MARKER +msg1=$(printf '%s' "$output1" | jq -r '.systemMessage // empty') +reason1=$(printf '%s' "$output1" | jq -r '.reason // empty') +if [[ "$msg1" == "Loop: Summary file still contains placeholders for round 0" ]] && \ + [[ "$reason1" == *"Work Summary Still Placeholder"* ]] && \ + [[ ! -e "$marker1" ]]; then + pass "placeholder summary is blocked before Codex runs" +else + fail "placeholder summary is blocked before Codex runs" "placeholder block without Codex marker" "msg=$msg1 marker=$([[ -e "$marker1" ]] && echo yes || echo no)" +fi + +# Test 2: concrete summary reaches Codex review path. +repo2="$TEST_DIR/repo-concrete" +marker2="$TEST_DIR/codex-ran-concrete" +summary_concrete='# Round 0 Summary + +## Work Completed +- Implemented the target change and captured validation evidence. + +## Files Changed +- hooks/loop-codex-stop-hook.sh + +## Validation +- bash -n hooks/loop-codex-stop-hook.sh: passed + +## Remaining Items +- None.' +create_loop_fixture "$repo2" "$summary_concrete" >/dev/null +export MOCK_CODEX_MARKER="$marker2" +output2=$(run_stop_hook "$repo2") +unset MOCK_CODEX_MARKER +if [[ -e "$marker2" ]] && [[ "$output2" == *"Codex found issues"* || "$output2" == *"Mainline Progress Verdict"* ]]; then + pass "concrete summary reaches Codex review" +else + fail "concrete summary reaches Codex review" "Codex marker created" "marker=$([[ -e "$marker2" ]] && echo yes || echo no) output=$output2" +fi + +# Test 3: concrete summary may mention scaffold tokens in prose/code blocks. +repo3="$TEST_DIR/repo-legitimate-mentions" +marker3="$TEST_DIR/codex-ran-legitimate-mentions" +summary_mentions='# Round 0 Summary + +## Work Completed +- Updated documentation that explains the BitLesson template string `Action: none|add|update`. +- Added validation output that quotes `[what changed and why]` as an example literal, not as the Notes scaffold line. + +## Files Changed +- prompt-template/block/work-summary-placeholder.md + +## Validation +```text +The docs mention [what changed and why] in prose and Action: none|add|update in a code block. +- [List created/modified files] +Action: none|add|update +Notes: [what changed and why] +``` + +## Remaining Items +- None.' +create_loop_fixture "$repo3" "$summary_mentions" >/dev/null +export MOCK_CODEX_MARKER="$marker3" +output3=$(run_stop_hook "$repo3") +unset MOCK_CODEX_MARKER +if [[ -e "$marker3" ]] && [[ "$output3" == *"Codex found issues"* || "$output3" == *"Mainline Progress Verdict"* ]]; then + pass "legitimate scaffold-token mentions reach Codex review" +else + fail "legitimate scaffold-token mentions reach Codex review" "Codex marker created" "marker=$([[ -e "$marker3" ]] && echo yes || echo no) output=$output3" +fi + +print_test_summary "Summary Placeholder Gate Test Summary" From 3385e30b350611c9960ca62697f789c5f4085116 Mon Sep 17 00:00:00 2001 From: "wentao.zhao" Date: Tue, 16 Jun 2026 18:04:08 +0800 Subject: [PATCH 2/4] Prioritize missing round contract guard Co-Authored-By: Claude Opus 4.8 (1M context) --- hooks/loop-codex-stop-hook.sh | 73 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 1ac5fbe9..06d27762 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -821,12 +821,46 @@ Please write your work summary to: {{SUMMARY_FILE}}" exit 0 fi +# Check Round Contract Exists +# ======================================== + +# Only enforce round contract when anti-drift is active (drift_status present in raw state). +# Legacy loops that pre-date the anti-drift feature will not have this field. +RAW_DRIFT_STATUS=$(echo "$RAW_FRONTMATTER" | grep "^drift_status:" || true) +if [[ "$IS_FINALIZE_PHASE" != "true" ]] && [[ -n "$RAW_DRIFT_STATUS" ]]; then + if [[ ! -f "$ROUND_CONTRACT_FILE" ]]; then + FALLBACK="# Round Contract Missing + +Before trying to exit, write the current round contract to: {{ROUND_CONTRACT_FILE}} + +The round contract must restate: +- The single mainline objective for this round +- The target ACs +- Which side issues are truly blocking +- Which side issues are queued and out of scope +- The success criteria for this round" + REASON=$(load_and_render_safe "$TEMPLATE_DIR" "block/round-contract-missing.md" "$FALLBACK" \ + "ROUND_CONTRACT_FILE=$ROUND_CONTRACT_FILE") + + jq -n \ + --arg reason "$REASON" \ + --arg msg "Loop: Round contract missing for round $CURRENT_ROUND" \ + '{ + "decision": "block", + "reason": $reason, + "systemMessage": $msg + }' + exit 0 + fi +fi + # Check Summary File Is Not Still the Scaffold # ============================================ # Round summary files are pre-created as editing targets. A file existing is -# therefore not proof that Claude actually summarized completed work. Block the -# common contract-only failure mode before spending a Codex review when the -# summary still contains template placeholders. +# therefore not proof that Claude actually summarized completed work. After the +# anti-drift contract precondition is satisfied, block the common contract-only +# failure mode before spending a Codex review when the summary still contains +# template placeholders. if [[ "$IS_FINALIZE_PHASE" != "true" ]]; then SUMMARY_PLACEHOLDERS=$(awk ' BEGIN { in_fence = 0 } @@ -865,39 +899,6 @@ Summary file: {{SUMMARY_FILE}}" fi fi -# Check Round Contract Exists -# ======================================== - -# Only enforce round contract when anti-drift is active (drift_status present in raw state). -# Legacy loops that pre-date the anti-drift feature will not have this field. -RAW_DRIFT_STATUS=$(echo "$RAW_FRONTMATTER" | grep "^drift_status:" || true) -if [[ "$IS_FINALIZE_PHASE" != "true" ]] && [[ -n "$RAW_DRIFT_STATUS" ]]; then - if [[ ! -f "$ROUND_CONTRACT_FILE" ]]; then - FALLBACK="# Round Contract Missing - -Before trying to exit, write the current round contract to: {{ROUND_CONTRACT_FILE}} - -The round contract must restate: -- The single mainline objective for this round -- The target ACs -- Which side issues are truly blocking -- Which side issues are queued and out of scope -- The success criteria for this round" - REASON=$(load_and_render_safe "$TEMPLATE_DIR" "block/round-contract-missing.md" "$FALLBACK" \ - "ROUND_CONTRACT_FILE=$ROUND_CONTRACT_FILE") - - jq -n \ - --arg reason "$REASON" \ - --arg msg "Loop: Round contract missing for round $CURRENT_ROUND" \ - '{ - "decision": "block", - "reason": $reason, - "systemMessage": $msg - }' - exit 0 - fi -fi - # ======================================== # Check BitLesson Delta Section (all non-finalize rounds) # ======================================== From 8775875a4522d13d474a2712cded51c1a3b47020 Mon Sep 17 00:00:00 2001 From: "wentao.zhao" Date: Tue, 16 Jun 2026 21:22:14 +0800 Subject: [PATCH 3/4] Prevent nested Codex review hooks Co-Authored-By: Claude Opus 4.8 (1M context) --- hooks/loop-codex-stop-hook.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 06d27762..56b942eb 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -27,6 +27,13 @@ DEFAULT_CODEX_TIMEOUT=5400 HOOK_INPUT=$(cat) +# Codex reviews launched by this hook can themselves trigger Stop hooks in Codex. +# Treat those nested hook invocations as internal review plumbing; otherwise the +# reviewer recursively launches another reviewer and the RLCR loop never advances. +if [[ "${HUMANIZE_INSIDE_CODEX_REVIEW:-}" == "1" ]]; then + exit 0 +fi + # NOTE: We intentionally do NOT check stop_hook_active here. # For iterative loops, stop_hook_active will be true when Claude is continuing # from a previous blocked stop. We WANT to run Codex review each iteration. @@ -1308,7 +1315,7 @@ Provider: codex echo "Running codex review with timeout ${CODEX_TIMEOUT}s in $PROJECT_ROOT (base: $review_base)..." >&2 CODEX_REVIEW_EXIT_CODE=0 - (cd "$PROJECT_ROOT" && run_with_timeout "$CODEX_TIMEOUT" codex review "${CODEX_DISABLE_HOOKS_ARGS[@]}" --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ + (cd "$PROJECT_ROOT" && HUMANIZE_INSIDE_CODEX_REVIEW=1 run_with_timeout "$CODEX_TIMEOUT" codex review "${CODEX_DISABLE_HOOKS_ARGS[@]}" --base "$review_base" "${CODEX_REVIEW_ARGS[@]}") \ > "$CODEX_REVIEW_LOG_FILE" 2>&1 || CODEX_REVIEW_EXIT_CODE=$? echo "Code review exit code: $CODEX_REVIEW_EXIT_CODE" >&2 @@ -1737,7 +1744,7 @@ echo "Codex command saved to: $CODEX_CMD_FILE" >&2 echo "Running summary review with timeout ${CODEX_TIMEOUT}s..." >&2 CODEX_EXIT_CODE=0 -printf '%s' "$CODEX_PROMPT_CONTENT" | run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ +printf '%s' "$CODEX_PROMPT_CONTENT" | HUMANIZE_INSIDE_CODEX_REVIEW=1 run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ > "$CODEX_STDOUT_FILE" 2> "$CODEX_STDERR_FILE" || CODEX_EXIT_CODE=$? echo "Codex exit code: $CODEX_EXIT_CODE" >&2 From 0a520527adfecece7b0d05b645dd7fdd944c5235 Mon Sep 17 00:00:00 2001 From: "wentao.zhao" Date: Tue, 16 Jun 2026 21:34:56 +0800 Subject: [PATCH 4/4] Fix Codex review stdin handling Co-Authored-By: Claude Opus 4.8 (1M context) --- hooks/loop-codex-stop-hook.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 56b942eb..b8e19c5c 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -25,8 +25,6 @@ DEFAULT_CODEX_TIMEOUT=5400 # Read Hook Input # ======================================== -HOOK_INPUT=$(cat) - # Codex reviews launched by this hook can themselves trigger Stop hooks in Codex. # Treat those nested hook invocations as internal review plumbing; otherwise the # reviewer recursively launches another reviewer and the RLCR loop never advances. @@ -34,6 +32,8 @@ if [[ "${HUMANIZE_INSIDE_CODEX_REVIEW:-}" == "1" ]]; then exit 0 fi +HOOK_INPUT=$(cat) + # NOTE: We intentionally do NOT check stop_hook_active here. # For iterative loops, stop_hook_active will be true when Claude is continuing # from a previous blocked stop. We WANT to run Codex review each iteration. @@ -1744,8 +1744,8 @@ echo "Codex command saved to: $CODEX_CMD_FILE" >&2 echo "Running summary review with timeout ${CODEX_TIMEOUT}s..." >&2 CODEX_EXIT_CODE=0 -printf '%s' "$CODEX_PROMPT_CONTENT" | HUMANIZE_INSIDE_CODEX_REVIEW=1 run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ - > "$CODEX_STDOUT_FILE" 2> "$CODEX_STDERR_FILE" || CODEX_EXIT_CODE=$? +HUMANIZE_INSIDE_CODEX_REVIEW=1 run_with_timeout "$CODEX_TIMEOUT" codex exec "${CODEX_DISABLE_HOOKS_ARGS[@]}" "${CODEX_EXEC_ARGS[@]}" - \ + < "$REVIEW_PROMPT_FILE" > "$CODEX_STDOUT_FILE" 2> "$CODEX_STDERR_FILE" || CODEX_EXIT_CODE=$? echo "Codex exit code: $CODEX_EXIT_CODE" >&2 echo "Codex stdout saved to: $CODEX_STDOUT_FILE" >&2