Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions hooks/loop-codex-stop-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Comment thread
zhwentao marked this conversation as resolved.
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor disabled BitLesson checks for review-only summaries

In --skip-impl/review-only loops, setup explicitly writes bitlesson_required: false so BitLesson formatting does not block the review workflow, and the review prompt only adds BitLesson instructions when that flag is true. This unconditional placeholder matcher still rejects the BitLesson scaffold lines, so a user can fill the real summary sections and still be blocked before codex review merely because the disabled BitLesson section contains Action: none|add|update or Notes: [what changed and why]. Guard these matches on BITLESSON_REQUIRED or omit the BitLesson scaffold when enforcement is disabled.

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)
# ========================================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions prompt-template/block/work-summary-placeholder.md
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.
6 changes: 6 additions & 0 deletions prompt-template/claude/drift-replan-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions prompt-template/claude/next-round-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
211 changes: 211 additions & 0 deletions tests/test-summary-placeholder-gate.sh
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"