-
Notifications
You must be signed in to change notification settings - Fork 97
Block RLCR placeholder summaries #217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,13 @@ DEFAULT_CODEX_TIMEOUT=5400 | |
| # Read Hook Input | ||
| # ======================================== | ||
|
|
||
| # 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 | ||
|
|
||
| HOOK_INPUT=$(cat) | ||
|
|
||
| # NOTE: We intentionally do NOT check stop_hook_active here. | ||
|
|
@@ -854,6 +861,51 @@ The round contract must restate: | |
| 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. 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 } | ||
| /^[[: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 } | ||
|
Comment on lines
+877
to
+878
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In Useful? React with 👍 / 👎. |
||
| ' "$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 BitLesson Delta Section (all non-finalize rounds) | ||
| # ======================================== | ||
|
|
@@ -1263,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 | ||
|
|
@@ -1692,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" | 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
Uh oh!
There was an error while loading. Please reload this page.