diff --git a/hooks/loop-codex-stop-hook.sh b/hooks/loop-codex-stop-hook.sh index 0682ff6e..f9b261b1 100755 --- a/hooks/loop-codex-stop-hook.sh +++ b/hooks/loop-codex-stop-hook.sh @@ -965,6 +965,37 @@ COMPLETED_ITERATIONS=$((CURRENT_ROUND + 1)) PREV_ROUND=$(( CURRENT_ROUND > 0 ? CURRENT_ROUND - 1 : 0 )) PREV_PREV_ROUND=$(( CURRENT_ROUND > 1 ? CURRENT_ROUND - 2 : 0 )) +# Integral component: accumulated commit history and recent round references +# Validate BASE_COMMIT is an ancestor of HEAD (not just a valid object) before using it in git log +if [[ -n "$BASE_COMMIT" ]] && git -C "$PROJECT_ROOT" merge-base --is-ancestor "$BASE_COMMIT" HEAD 2>/dev/null; then + COMMIT_HISTORY=$(git -C "$PROJECT_ROOT" log --oneline --no-decorate --reverse "$BASE_COMMIT"..HEAD 2>/dev/null | tail -80) +else + COMMIT_HISTORY=$(git -C "$PROJECT_ROOT" log --oneline --no-decorate --reverse -30 2>/dev/null) + # Annotate so Codex knows this is not the full loop history + [[ -n "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(base commit unavailable, showing recent branch commits) +${COMMIT_HISTORY}" +fi +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +RECENT_ROUND_FILES="" +for (( r = CURRENT_ROUND - 1; r >= 0 && r >= CURRENT_ROUND - 3; r-- )); do + RECENT_ROUND_FILES+="- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-summary.md +- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-review-result.md +" +done +[[ -z "$RECENT_ROUND_FILES" ]] && RECENT_ROUND_FILES="(first round, no prior history)" + +COMMIT_HISTORY_SECTION_FALLBACK="## Development History (Integral Context) +\`\`\` +${COMMIT_HISTORY} +\`\`\` +### Recent Round Files +Read these files before conducting your review to understand the trajectory of work: +${RECENT_ROUND_FILES}" +COMMIT_HISTORY_SECTION=$(load_and_render_safe "$TEMPLATE_DIR" "codex/commit-history-section.md" "$COMMIT_HISTORY_SECTION_FALLBACK" \ + "COMMIT_HISTORY=$COMMIT_HISTORY" \ + "RECENT_ROUND_FILES=$RECENT_ROUND_FILES") + # Build the review prompt FULL_ALIGNMENT_FALLBACK="# Full Alignment Review (Round {{CURRENT_ROUND}}) @@ -973,6 +1004,8 @@ Review Claude's work against the plan and goal tracker. Check all goals are bein ## Claude's Summary {{SUMMARY_CONTENT}} +{{COMMIT_HISTORY_SECTION}} + {{GOAL_TRACKER_UPDATE_SECTION}} Write your review to {{REVIEW_RESULT_FILE}}. End with COMPLETE if done, or list issues." @@ -984,6 +1017,8 @@ Review Claude's work for this round. ## Claude's Summary {{SUMMARY_CONTENT}} +{{COMMIT_HISTORY_SECTION}} + {{GOAL_TRACKER_UPDATE_SECTION}} Write your review to {{REVIEW_RESULT_FILE}}. End with COMPLETE if done, or list issues." @@ -997,6 +1032,7 @@ if [[ "$FULL_ALIGNMENT_CHECK" == "true" ]]; then "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ "DOCS_PATH=$DOCS_PATH" \ "GOAL_TRACKER_UPDATE_SECTION=$GOAL_TRACKER_UPDATE_SECTION" \ + "COMMIT_HISTORY_SECTION=$COMMIT_HISTORY_SECTION" \ "COMPLETED_ITERATIONS=$COMPLETED_ITERATIONS" \ "LOOP_TIMESTAMP=$LOOP_TIMESTAMP" \ "PREV_ROUND=$PREV_ROUND" \ @@ -1013,6 +1049,7 @@ else "GOAL_TRACKER_FILE=$GOAL_TRACKER_FILE" \ "DOCS_PATH=$DOCS_PATH" \ "GOAL_TRACKER_UPDATE_SECTION=$GOAL_TRACKER_UPDATE_SECTION" \ + "COMMIT_HISTORY_SECTION=$COMMIT_HISTORY_SECTION" \ "COMPLETED_ITERATIONS=$COMPLETED_ITERATIONS" \ "LOOP_TIMESTAMP=$LOOP_TIMESTAMP" \ "PREV_ROUND=$PREV_ROUND" \ diff --git a/prompt-template/codex/commit-history-section.md b/prompt-template/codex/commit-history-section.md new file mode 100644 index 00000000..48ca2e35 --- /dev/null +++ b/prompt-template/codex/commit-history-section.md @@ -0,0 +1,12 @@ +## Development History (Integral Context) + +Accumulated commits since loop start (oldest first): +``` +{{COMMIT_HISTORY}} +``` + +### Recent Round Files +Read these files before conducting your review to understand the trajectory of work: +{{RECENT_ROUND_FILES}} + +Use this history to identify patterns across rounds: recurring issues, stalled progress, or drift from the mainline objective. Weight recent rounds more heavily but watch for systemic trends in the full commit log. diff --git a/prompt-template/codex/full-alignment-review.md b/prompt-template/codex/full-alignment-review.md index 02997dd8..4367810e 100644 --- a/prompt-template/codex/full-alignment-review.md +++ b/prompt-template/codex/full-alignment-review.md @@ -16,6 +16,8 @@ You MUST read this plan file first to understand the full scope of work before c --- +{{COMMIT_HISTORY_SECTION}} + ## Part 1: Goal Tracker Audit (MANDATORY) Read @{{GOAL_TRACKER_FILE}} and verify: diff --git a/prompt-template/codex/regular-review.md b/prompt-template/codex/regular-review.md index 7db26ea2..4d4a8680 100644 --- a/prompt-template/codex/regular-review.md +++ b/prompt-template/codex/regular-review.md @@ -17,6 +17,8 @@ Below is Claude's summary of the work completed: --- +{{COMMIT_HISTORY_SECTION}} + ## Part 1: Implementation Review - Your task is to conduct a deep critical review, focusing on finding implementation issues and identifying gaps between "plan-design" and actual implementation. diff --git a/tests/test-commit-history-section.sh b/tests/test-commit-history-section.sh new file mode 100755 index 00000000..7177f5e8 --- /dev/null +++ b/tests/test-commit-history-section.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# +# Test script for the Integral (I) component: commit-history-section +# +# Validates: +# 1. Round 0: "(no commits yet)" and "(first round, no prior history)" +# 2. Round 2+: commit log and round file references rendered correctly +# 3. Corrupted BASE_COMMIT: graceful fallback with annotation +# 4. Template missing: fallback renders the full section including round files +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +source "$SCRIPT_DIR/test-helpers.sh" +source "$PROJECT_ROOT/hooks/lib/template-loader.sh" + +TEMPLATE_DIR="$PROJECT_ROOT/prompt-template" + +echo "========================================" +echo "Testing commit-history-section (I component)" +echo "========================================" +echo "" + +# ======================================== +# Setup: create a temporary git repo +# ======================================== +setup_test_dir +init_test_git_repo "$TEST_DIR/repo" + +# ======================================== +# Test 1: Round 0 - no commits since base, first round +# ======================================== +echo "Test 1: Round 0 - no commits, first round" + +CURRENT_ROUND=0 +BASE_COMMIT=$(git -C "$TEST_DIR/repo" rev-parse HEAD) + +# No commits since BASE_COMMIT..HEAD (same commit) +COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse "$BASE_COMMIT"..HEAD 2>/dev/null | tail -80) +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +RECENT_ROUND_FILES="" +LOOP_TIMESTAMP="2026-01-01_00-00-00" +for (( r = CURRENT_ROUND - 1; r >= 0 && r >= CURRENT_ROUND - 3; r-- )); do + RECENT_ROUND_FILES+="- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-summary.md +- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-review-result.md +" +done +[[ -z "$RECENT_ROUND_FILES" ]] && RECENT_ROUND_FILES="(first round, no prior history)" + +RESULT=$(load_and_render_safe "$TEMPLATE_DIR" "codex/commit-history-section.md" "FALLBACK" \ + "COMMIT_HISTORY=$COMMIT_HISTORY" \ + "RECENT_ROUND_FILES=$RECENT_ROUND_FILES") + +if echo "$RESULT" | grep -q "(no commits yet)" && echo "$RESULT" | grep -q "(first round, no prior history)"; then + pass "Round 0 shows correct placeholders" +else + fail "Round 0 placeholders" "(no commits yet) and (first round, no prior history)" "$RESULT" +fi + +# ======================================== +# Test 2: Round 3 - with commits and round history +# ======================================== +echo "" +echo "Test 2: Round 3 - commits and round file references" + +# Make some commits +cd "$TEST_DIR/repo" +echo "feat1" > feat1.txt && git add feat1.txt && git commit -q -m "feat: add feature 1" +echo "feat2" > feat2.txt && git add feat2.txt && git commit -q -m "feat: add feature 2" +echo "fix1" > fix1.txt && git add fix1.txt && git commit -q -m "fix: resolve bug in feature 1" +cd - > /dev/null + +CURRENT_ROUND=3 +COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse "$BASE_COMMIT"..HEAD 2>/dev/null | tail -80) +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +RECENT_ROUND_FILES="" +for (( r = CURRENT_ROUND - 1; r >= 0 && r >= CURRENT_ROUND - 3; r-- )); do + RECENT_ROUND_FILES+="- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-summary.md +- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-review-result.md +" +done +[[ -z "$RECENT_ROUND_FILES" ]] && RECENT_ROUND_FILES="(first round, no prior history)" + +RESULT=$(load_and_render_safe "$TEMPLATE_DIR" "codex/commit-history-section.md" "FALLBACK" \ + "COMMIT_HISTORY=$COMMIT_HISTORY" \ + "RECENT_ROUND_FILES=$RECENT_ROUND_FILES") + +HAS_COMMITS=true +HAS_ROUNDS=true + +echo "$RESULT" | grep -q "feat: add feature 1" || HAS_COMMITS=false +echo "$RESULT" | grep -q "feat: add feature 2" || HAS_COMMITS=false +echo "$RESULT" | grep -q "fix: resolve bug in feature 1" || HAS_COMMITS=false + +echo "$RESULT" | grep -q "round-2-summary.md" || HAS_ROUNDS=false +echo "$RESULT" | grep -q "round-1-summary.md" || HAS_ROUNDS=false +echo "$RESULT" | grep -q "round-0-summary.md" || HAS_ROUNDS=false +echo "$RESULT" | grep -q "round-2-review-result.md" || HAS_ROUNDS=false + +if [[ "$HAS_COMMITS" == "true" ]]; then + pass "Round 3 shows all 3 commits" +else + fail "Round 3 commits" "3 commit messages" "$RESULT" +fi + +if [[ "$HAS_ROUNDS" == "true" ]]; then + pass "Round 3 shows round 0-2 file references" +else + fail "Round 3 round files" "round-0/1/2 summary and review files" "$RESULT" +fi + +# ======================================== +# Test 3: Corrupted BASE_COMMIT - nonexistent object +# ======================================== +echo "" +echo "Test 3: Corrupted BASE_COMMIT graceful fallback" + +BAD_COMMIT="deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + +# Simulate the exact logic from the stop hook (merge-base --is-ancestor) +if [[ -n "$BAD_COMMIT" ]] && git -C "$TEST_DIR/repo" merge-base --is-ancestor "$BAD_COMMIT" HEAD 2>/dev/null; then + COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse "$BAD_COMMIT"..HEAD 2>/dev/null | tail -80) +else + COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse -30 2>/dev/null) + [[ -n "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(base commit unavailable, showing recent branch commits) +${COMMIT_HISTORY}" +fi +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +if echo "$COMMIT_HISTORY" | grep -q "base commit unavailable"; then + pass "Corrupted BASE_COMMIT triggers annotation" +else + fail "Corrupted BASE_COMMIT annotation" "base commit unavailable" "$COMMIT_HISTORY" +fi + +if echo "$COMMIT_HISTORY" | grep -q "feat: add feature"; then + pass "Corrupted BASE_COMMIT still shows recent commits" +else + fail "Corrupted BASE_COMMIT recent commits" "recent branch commits" "$COMMIT_HISTORY" +fi + +# Verify no crash (we got here = no set -e crash) +pass "Corrupted BASE_COMMIT did not crash (set -e safe)" + +# ======================================== +# Test 3b: Valid but unrelated commit (not ancestor of HEAD) +# ======================================== +echo "" +echo "Test 3b: Valid but unrelated BASE_COMMIT (orphan branch)" + +# Create an orphan branch with its own commit, then switch back +cd "$TEST_DIR/repo" +ORIG_BRANCH=$(git rev-parse --abbrev-ref HEAD) +git checkout -q --orphan orphan-test +echo "orphan" > orphan.txt && git add orphan.txt && git commit -q -m "orphan commit" +ORPHAN_COMMIT=$(git rev-parse HEAD) +git checkout -q "$ORIG_BRANCH" +cd - > /dev/null + +# ORPHAN_COMMIT exists but is NOT an ancestor of HEAD +if [[ -n "$ORPHAN_COMMIT" ]] && git -C "$TEST_DIR/repo" merge-base --is-ancestor "$ORPHAN_COMMIT" HEAD 2>/dev/null; then + COMMIT_HISTORY="should not reach here" +else + COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse -30 2>/dev/null) + [[ -n "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(base commit unavailable, showing recent branch commits) +${COMMIT_HISTORY}" +fi +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +if echo "$COMMIT_HISTORY" | grep -q "base commit unavailable"; then + pass "Unrelated valid commit triggers annotation" +else + fail "Unrelated valid commit annotation" "base commit unavailable" "$COMMIT_HISTORY" +fi + +# ======================================== +# Test 4: Missing template - fallback renders full section +# ======================================== +echo "" +echo "Test 4: Missing template fallback renders full section" + +CURRENT_ROUND=2 +COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse "$BASE_COMMIT"..HEAD 2>/dev/null | tail -80) + +RECENT_ROUND_FILES="" +for (( r = CURRENT_ROUND - 1; r >= 0 && r >= CURRENT_ROUND - 3; r-- )); do + RECENT_ROUND_FILES+="- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-summary.md +- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-review-result.md +" +done + +# Use the exact fallback format from the stop hook +COMMIT_HISTORY_SECTION_FALLBACK="## Development History (Integral Context) +\`\`\` +${COMMIT_HISTORY} +\`\`\` +### Recent Round Files +Read these files before conducting your review to understand the trajectory of work: +${RECENT_ROUND_FILES}" + +# Point to a non-existent template to force fallback +RESULT=$(load_and_render_safe "$TEMPLATE_DIR" "codex/non-existent-template.md" "$COMMIT_HISTORY_SECTION_FALLBACK" \ + "COMMIT_HISTORY=$COMMIT_HISTORY" \ + "RECENT_ROUND_FILES=$RECENT_ROUND_FILES") + +FALLBACK_OK=true +echo "$RESULT" | grep -q "Development History" || FALLBACK_OK=false +echo "$RESULT" | grep -q "feat: add feature 1" || FALLBACK_OK=false +echo "$RESULT" | grep -q "Recent Round Files" || FALLBACK_OK=false +echo "$RESULT" | grep -q "round-1-summary.md" || FALLBACK_OK=false +echo "$RESULT" | grep -q "round-0-review-result.md" || FALLBACK_OK=false +echo "$RESULT" | grep -q "Read these files" || FALLBACK_OK=false + +if [[ "$FALLBACK_OK" == "true" ]]; then + pass "Fallback renders full section with commits, round files, and directive" +else + fail "Fallback full section" "commits + round files + directive" "$RESULT" +fi + +# ======================================== +# Test 5: Round 1 - only 1 prior round (boundary) +# ======================================== +echo "" +echo "Test 5: Round 1 - only 1 prior round" + +CURRENT_ROUND=1 +RECENT_ROUND_FILES="" +for (( r = CURRENT_ROUND - 1; r >= 0 && r >= CURRENT_ROUND - 3; r-- )); do + RECENT_ROUND_FILES+="- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-summary.md +- @.humanize/rlcr/${LOOP_TIMESTAMP}/round-${r}-review-result.md +" +done +[[ -z "$RECENT_ROUND_FILES" ]] && RECENT_ROUND_FILES="(first round, no prior history)" + +if echo "$RECENT_ROUND_FILES" | grep -q "round-0-summary.md" && \ + ! echo "$RECENT_ROUND_FILES" | grep -q "round-1-"; then + pass "Round 1 references only round 0" +else + fail "Round 1 boundary" "only round-0 references" "$RECENT_ROUND_FILES" +fi + +# ======================================== +# Test 6: Empty BASE_COMMIT (legacy loop) +# ======================================== +echo "" +echo "Test 6: Empty BASE_COMMIT fallback" + +EMPTY_BASE="" +if [[ -n "$EMPTY_BASE" ]] && git -C "$TEST_DIR/repo" merge-base --is-ancestor "$EMPTY_BASE" HEAD 2>/dev/null; then + COMMIT_HISTORY="should not reach here" +else + COMMIT_HISTORY=$(git -C "$TEST_DIR/repo" log --oneline --no-decorate --reverse -30 2>/dev/null) + [[ -n "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(base commit unavailable, showing recent branch commits) +${COMMIT_HISTORY}" +fi +[[ -z "$COMMIT_HISTORY" ]] && COMMIT_HISTORY="(no commits yet)" + +if echo "$COMMIT_HISTORY" | grep -q "base commit unavailable"; then + pass "Empty BASE_COMMIT triggers annotation" +else + fail "Empty BASE_COMMIT annotation" "base commit unavailable" "$COMMIT_HISTORY" +fi + +# ======================================== +# Summary +# ======================================== +print_test_summary "Commit History Section (I Component) Tests"