Skip to content
Merged
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
52 changes: 51 additions & 1 deletion hooks/lib/loop-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ git_adds_humanize() {
# Check for direct .humanize reference (blocked regardless of other flags)
# Handles: .humanize, ./.humanize, path/to/.humanize, ".humanize", '.humanize'
# Pattern matches .humanize at start, after space, after / or ./ AND followed by end, /, or space
# This avoids over-blocking .humanizeconfig or .humanize-backup
# This avoids over-blocking .humanizeconfig or .humanize-backup.
if echo "$add_args_normalized" | grep -qE '(^|[[:space:]]|/)\.humanize($|/|[[:space:]])'; then
return 0
fi
Expand Down Expand Up @@ -1319,6 +1319,56 @@ IMPORTANT: The commit message must NOT contain the literal string \".humanize\"
load_and_render_safe "$TEMPLATE_DIR" "block/git-add-humanize.md" "$fallback"
}

# Return success if local Humanize runtime state has entered git tracking or the index.
# Untracked .humanize state is allowed; tracked or staged state must be blocked.
# Usage: git_has_tracked_humanize_state [project_root]
#
# Intentionally scoped to .humanize/ to stay consistent with git_adds_humanize,
# which explicitly allows unrelated paths like .humanize-backup or
# .humanizeconfig (see tests/test-humanize-escape.sh). ls-files covers both
# committed entries and paths staged via git add; paths the user has staged for
# removal via git rm --cached are correctly omitted so the user can unstick
# themselves without being re-blocked.
git_has_tracked_humanize_state() {
local project_root="${1:-.}"

if [[ ! -d "$project_root/.git" ]] && ! git -C "$project_root" rev-parse --git-dir >/dev/null 2>&1; then
return 1
fi

if git -C "$project_root" ls-files -- .humanize 2>/dev/null | grep -q '.'; then
return 0
fi

return 1
}

# Standard message for blocking tracked/staged .humanize state.
# Usage: git_tracked_humanize_blocked_message
git_tracked_humanize_blocked_message() {
local fallback="# Tracked Humanize State Blocked

Detected tracked or staged files under \`.humanize/\`.

These files are local Humanize loop state and must remain outside version control.

## Required Fix

1. Remove Humanize state from the index:

git rm --cached -r .humanize

2. Keep only real project files staged.
3. Retry the stop action after the local state is no longer tracked.

## Important

- Do NOT use \`git add -f\` on Humanize state files.
- Do NOT commit RLCR trackers, round summaries, contracts, or cancel/finalize markers."

load_and_render_safe "$TEMPLATE_DIR" "block/git-tracked-humanize.md" "$fallback"
}

# Standard message for blocking direct execution of hook scripts
# Usage: stop_hook_direct_execution_blocked_message
stop_hook_direct_execution_blocked_message() {
Expand Down
15 changes: 15 additions & 0 deletions hooks/loop-codex-stop-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,21 @@ if [[ "$GIT_IS_REPO" == "true" ]]; then
GIT_ISSUES=""
SPECIAL_NOTES=""

if git_has_tracked_humanize_state "$PROJECT_ROOT"; then
cleanup_stale_index_lock
REASON=$(git_tracked_humanize_blocked_message)

jq -n \
--arg reason "$REASON" \
--arg msg "Loop: Blocked - tracked Humanize state detected, remove it from git first" \
'{
"decision": "block",
"reason": $reason,
"systemMessage": $msg
}'
exit 0
fi

# Check for uncommitted changes (staged or unstaged) using cached status.
# Exclude untracked .humanize/ paths and .humanize-* dash-separated legacy
# variants from the dirty determination because local plugin state under
Expand Down
4 changes: 3 additions & 1 deletion prompt-template/block/git-not-clean.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ You are trying to stop, but you have **{{GIT_ISSUES}}**.
**Required Actions**:
0. If the `code-simplifier` plugin is installed, use it to review and simplify your code before committing. Invoke via: `/code-simplifier`, `@agent-code-simplifier`, or `@code-simplifier:code-simplifier (agent)`
1. Review untracked files - add build artifacts to `.gitignore`
2. Stage real changes: `git add <files>` (or `git add -A` if all files should be tracked)
2. Stage only real changes with specific paths: `git add <files>`
3. Commit with a descriptive message following project conventions

**Important Rules**:
- Do NOT use `git add -A`, `git add --all`, or `git add .` during an active RLCR loop
- Never stage `.humanize/` or legacy `.humanize-*` loop artifacts
- Commit message must follow project conventions
- AI tools (Claude, Codex, etc.) must NOT have authorship in commits
- Do NOT include `Co-Authored-By: Claude` or similar AI attribution
Expand Down
19 changes: 19 additions & 0 deletions prompt-template/block/git-tracked-humanize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Tracked Humanize State Blocked

Detected tracked or staged files under `.humanize/`.

These files are local Humanize loop state and must remain outside version control.

## Required Fix

1. Remove Humanize state from the index:

git rm --cached -r .humanize

2. Keep only real project files staged.
3. Retry the stop action after the local state is no longer tracked.

## Important

- Do NOT use `git add -f` on Humanize state files.
- Do NOT commit RLCR trackers, round summaries, contracts, or cancel/finalize markers.
5 changes: 5 additions & 0 deletions tests/test-humanize-escape.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ assert_blocks "git add ./.humanize/" "Block: ./.humanize/ with trailing slash"
assert_blocks "git add ./.humanize/file.md" "Block: ./.humanize/file.md"
assert_blocks "git add path/to/.humanize" "Block: path/to/.humanize"
assert_blocks "git add ../project/.humanize" "Block: ../project/.humanize"
assert_blocks "git add .humanize/rlcr/2026-03-01_00-00-00/goal-tracker.md" "Block: RLCR goal tracker path"
assert_blocks "git add .humanize/rlcr/2026-03-01_00-00-00/round-3-summary.md" "Block: RLCR round summary path"
assert_blocks "git add .humanize/rlcr/2026-03-01_00-00-00/round-3-contract.md" "Block: RLCR round contract path"

# ========================================
# Test Group 2: Quoted Path Variants
Expand All @@ -98,6 +101,7 @@ assert_blocks "git add '.humanize'" "Block: single-quoted .humanize"
assert_blocks 'git add "./.humanize"' "Block: double-quoted ./.humanize"
assert_blocks "git add './.humanize'" "Block: single-quoted ./.humanize"
assert_blocks 'git add "path/to/.humanize"' "Block: double-quoted path/to/.humanize"
assert_blocks 'git add ".humanize/rlcr/2026-03-01_00-00-00/goal-tracker.md"' "Block: double-quoted RLCR file path"

# ========================================
# Test Group 3: Combined Force and Path Variants
Expand All @@ -109,6 +113,7 @@ echo ""
assert_blocks "git add -f ./.humanize" "Block: -f with ./.humanize"
assert_blocks "git add --force ./.humanize" "Block: --force with ./.humanize"
assert_blocks 'git add -f ".humanize"' "Block: -f with quoted .humanize"
assert_blocks "git add -f .humanize/rlcr/2026-03-01_00-00-00/goal-tracker.md" "Block: -f with RLCR goal tracker"

# Force flag with broad scope (blocks gitignore bypass)
assert_blocks "git add -f ." "Block: -f . (force with current dir)"
Expand Down
80 changes: 71 additions & 9 deletions tests/test-stop-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,30 +115,92 @@ else
fail "rlcr-stop-gate --project-root output contains expected block reason" "output containing BLOCK:" "$OUTPUT2"
fi

# Test 3: No active loop -> gate allows exit (exit 0)
# Test 3: Tracked Humanize state blocks before normal loop validation
T3_DIR="$TEST_DIR/t3"
mkdir -p "$T3_DIR/empty-project"
mkdir -p "$T3_DIR"
setup_active_loop_fixture "$T3_DIR/project"
echo "tracked" > "$T3_DIR/project/.humanize/rlcr/2026-03-01_00-00-00/goal-tracker.md"
git -C "$T3_DIR/project" add -f .humanize/rlcr/2026-03-01_00-00-00/goal-tracker.md

set +e
(
cd "$T3_DIR/empty-project"
cd "$T3_DIR/project"
"$GATE_SCRIPT"
) > "$T3_DIR/out.txt" 2>&1
EXIT3=$?
set -e

if [[ "$EXIT3" -eq 0 ]]; then
pass "rlcr-stop-gate exits 0 when no active loop exists"
if [[ "$EXIT3" -eq 10 ]]; then
pass "rlcr-stop-gate blocks tracked Humanize state"
else
OUTPUT3=$(cat "$T3_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate exits 0 when no active loop exists" "exit 0" "exit $EXIT3; output: $OUTPUT3"
fail "rlcr-stop-gate blocks tracked Humanize state" "exit 10" "exit $EXIT3; output: $OUTPUT3"
fi

if grep -q "^ALLOW:" "$T3_DIR/out.txt" 2>/dev/null; then
pass "rlcr-stop-gate reports ALLOW when no active loop"
if grep -q "Tracked Humanize State Blocked" "$T3_DIR/out.txt" 2>/dev/null; then
pass "rlcr-stop-gate reports tracked Humanize state with dedicated reason"
else
OUTPUT3=$(cat "$T3_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate reports ALLOW when no active loop" "output containing ALLOW:" "$OUTPUT3"
fail "rlcr-stop-gate reports tracked Humanize state with dedicated reason" "output containing Tracked Humanize State Blocked" "$OUTPUT3"
fi

# Test 4: Unrelated dot-prefixed files that happen to start with .humanize-
# must not be treated as loop state. .humanize-backup and .humanizeconfig are
# explicitly allowed by the git add validator (tests/test-humanize-escape.sh);
# the tracked-state guard must stay consistent and ignore them.
T4_DIR="$TEST_DIR/t4"
mkdir -p "$T4_DIR"
setup_active_loop_fixture "$T4_DIR/project"
echo "not loop state" > "$T4_DIR/project/.humanize-backup"
echo "not loop state" > "$T4_DIR/project/.humanizeconfig"
git -C "$T4_DIR/project" add -f .humanize-backup .humanizeconfig

set +e
(
cd "$T4_DIR/project"
"$GATE_SCRIPT"
) > "$T4_DIR/out.txt" 2>&1
EXIT4=$?
set -e

if [[ "$EXIT4" -eq 10 ]]; then
pass "rlcr-stop-gate does not confuse .humanize-backup with loop state"
else
OUTPUT4=$(cat "$T4_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate does not confuse .humanize-backup with loop state" "exit 10" "exit $EXIT4; output: $OUTPUT4"
fi

if ! grep -q "Tracked Humanize State Blocked" "$T4_DIR/out.txt" 2>/dev/null; then
pass "rlcr-stop-gate does not emit tracked-state reason for .humanize-backup"
else
OUTPUT4=$(cat "$T4_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate does not emit tracked-state reason for .humanize-backup" "no Tracked Humanize State Blocked line" "$OUTPUT4"
fi

# Test 5: No active loop -> gate allows exit (exit 0)
T5_DIR="$TEST_DIR/t5"
mkdir -p "$T5_DIR/empty-project"

set +e
(
cd "$T5_DIR/empty-project"
"$GATE_SCRIPT"
) > "$T5_DIR/out.txt" 2>&1
EXIT5=$?
set -e

if [[ "$EXIT5" -eq 0 ]]; then
pass "rlcr-stop-gate exits 0 when no active loop exists"
else
OUTPUT5=$(cat "$T5_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate exits 0 when no active loop exists" "exit 0" "exit $EXIT5; output: $OUTPUT5"
fi

if grep -q "^ALLOW:" "$T5_DIR/out.txt" 2>/dev/null; then
pass "rlcr-stop-gate reports ALLOW when no active loop"
else
OUTPUT5=$(cat "$T5_DIR/out.txt" 2>/dev/null || true)
fail "rlcr-stop-gate reports ALLOW when no active loop" "output containing ALLOW:" "$OUTPUT5"
fi

print_test_summary "RLCR Stop Gate Wrapper Test Summary"
Expand Down
Loading