From 326a570cf4ec25b570de7f43f1d927efcb353bd8 Mon Sep 17 00:00:00 2001 From: jito Date: Thu, 5 Feb 2026 08:26:59 +0900 Subject: [PATCH] fix(ralph-loop): fix hooks matcher and stdin handling Critical bugs that prevented /ralph-loop from working: 1. hooks.json: Change matcher from "ralph-loop-active" to "*" - The invalid matcher prevented hooks from ever executing - Wildcard matcher allows hooks to run, with state file check inside 2. session-start-hook.sh: Read SESSION_ID from stdin - Add --- plugins/ralph-loop/CLAUDE.md | 64 --------- plugins/ralph-loop/hooks/CLAUDE.md | 51 ------- plugins/ralph-loop/hooks/hooks.json | 4 +- .../ralph-loop/hooks/session-start-hook.sh | 11 +- .../ralph-loop/tests/ralph-loop-specific.bats | 125 ++++++++++++++++++ 5 files changed, 136 insertions(+), 119 deletions(-) delete mode 100644 plugins/ralph-loop/CLAUDE.md delete mode 100644 plugins/ralph-loop/hooks/CLAUDE.md diff --git a/plugins/ralph-loop/CLAUDE.md b/plugins/ralph-loop/CLAUDE.md deleted file mode 100644 index 71ab3aca..00000000 --- a/plugins/ralph-loop/CLAUDE.md +++ /dev/null @@ -1,64 +0,0 @@ - - - -# ralph-loop - -## Purpose -Continuous self-referential AI loops for interactive iterative development, implementing the Ralph Wiggum technique. - -## Key Files - -| File | Description | -|------|-------------| -| `README.md` | Plugin documentation | -| `.claude-plugin/plugin.json` | Plugin manifest | -| `hooks/hooks.json` | Hook configuration | -| `scripts/lib/` | Utility libraries | - -## Commands - -| Command | Purpose | -|---------|---------| -| `ralph-loop.md` | Start Ralph Loop with prompt and iteration count | -| `cancel-ralph.md` | Cancel active Ralph Loop | -| `help.md` | Display Ralph Loop help | - -## Subdirectories - -| Directory | Purpose | -|-----------|---------| -| `commands/` | Ralph Loop commands | -| `hooks/` | SessionStart/SessionStop hooks | -| `scripts/` | Setup and control scripts | -| `scripts/lib/` | Shared libraries | -| `tests/` | Plugin tests | - -## For AI Agents - -### Working In This Directory -- Stop hook intercepts session termination -- Feeds last assistant output as next prompt -- Uses state files in `~/.claude/ralph-loop/` -- Supports completion promises (`TAG`) - -### Testing Requirements -- Test loop iteration mechanics -- Verify completion detection -- Test cancellation functionality -- Ensure state cleanup on completion - -### Common Patterns -- Hook scripts use `set -euo pipefail` -- State files use `.local.md` extension -- Session ID in environment -- Maximum iterations configurable - -## Dependencies - -### Internal -- `tests/ralph-loop/` - Plugin tests (if exists) - -### External -- None (self-contained) - - diff --git a/plugins/ralph-loop/hooks/CLAUDE.md b/plugins/ralph-loop/hooks/CLAUDE.md deleted file mode 100644 index 46472d86..00000000 --- a/plugins/ralph-loop/hooks/CLAUDE.md +++ /dev/null @@ -1,51 +0,0 @@ - - - -# hooks - -## Purpose -Session hooks for Ralph Loop - SessionStart setup and SessionStop looping mechanism. - -## Key Files - -| File | Description | -|------|-------------| -| `hooks.json` | Hook configuration | -| `session-start-hook.sh` | Initialize Ralph Loop state on session start | -| `session-env.sh` | Set environment variables for session tracking | -| `stop-hook.sh` | Intercept session stop and continue loop | - -## Subdirectories - -None - -## For AI Agents - -### Working In This Directory -- SessionStart creates state directory and files -- SessionStop is intercepted to continue iteration -- State files track iteration count and completion -- Completes when `TAG` detected or max iterations reached - -### Testing Requirements -- Test session start initialization -- Verify stop hook interception -- Test completion detection -- Ensure state cleanup on completion - -### Common Patterns -- `set -euo pipefail` for error detection -- State in `~/.claude/ralph-loop/` -- Session ID from `${CLAUDE_SESSION_ID}` -- Last assistant output fed as next prompt - -## Dependencies - -### External -- None (self-contained) - -### Internal -- `../commands/` - Commands that start/control loop -- `../scripts/lib/` - Shared utilities - - diff --git a/plugins/ralph-loop/hooks/hooks.json b/plugins/ralph-loop/hooks/hooks.json index 800aa7ca..69a3bdcb 100644 --- a/plugins/ralph-loop/hooks/hooks.json +++ b/plugins/ralph-loop/hooks/hooks.json @@ -4,7 +4,7 @@ "hooks": { "SessionStart": [ { - "matcher": "ralph-loop-active", + "matcher": "*", "hooks": [ { "type": "command", @@ -15,7 +15,7 @@ ], "Stop": [ { - "matcher": "ralph-loop-active", + "matcher": "*", "hooks": [ { "type": "command", diff --git a/plugins/ralph-loop/hooks/session-start-hook.sh b/plugins/ralph-loop/hooks/session-start-hook.sh index ba3c6c59..db018f71 100755 --- a/plugins/ralph-loop/hooks/session-start-hook.sh +++ b/plugins/ralph-loop/hooks/session-start-hook.sh @@ -7,8 +7,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../scripts/lib/state.sh" -# Extract session_id from stdin (remove useless use of cat) -SESSION_ID=$(jq -r '.session_id') +# Extract session_id from stdin +SESSION_ID=$(jq -r '.session_id' > "$ENV_FILE" +elif [[ ! -f "$ENV_FILE" ]]; then + # File doesn't exist (just created above), create it with content + # Safe to use unquoted since we validated SESSION_ID contains only safe characters + echo "export RALPH_SESSION_ID=$SESSION_ID" > "$ENV_FILE" fi exit 0 diff --git a/plugins/ralph-loop/tests/ralph-loop-specific.bats b/plugins/ralph-loop/tests/ralph-loop-specific.bats index c86ce025..8f43ccc3 100644 --- a/plugins/ralph-loop/tests/ralph-loop-specific.bats +++ b/plugins/ralph-loop/tests/ralph-loop-specific.bats @@ -531,3 +531,128 @@ EOF # This prevents "unbound variable" errors when set -u is enabled grep -q 'PROMPT_PARTS\[\*\]:-' "$script" } + +# ============================================================================ +# Bug Fix Tests - These tests should FAIL before the fix +# ============================================================================ + +@test "ralph-loop: hooks.json uses wildcard matcher for SessionStart hook" { + local hooks_json="${PLUGIN_DIR}/hooks/hooks.json" + [ -f "$hooks_json" ] + ensure_jq + + # The matcher should be "*" to run on all sessions + # The hook script itself checks if state file exists + local matcher + matcher=$($JQ_BIN -r '.hooks.SessionStart[0].matcher' "$hooks_json") + + # This test will FAIL until the bug is fixed + [ "$matcher" = "*" ] +} + +@test "ralph-loop: hooks.json uses wildcard matcher for Stop hook" { + local hooks_json="${PLUGIN_DIR}/hooks/hooks.json" + [ -f "$hooks_json" ] + ensure_jq + + # The matcher should be "*" to run on all sessions + local matcher + matcher=$($JQ_BIN -r '.hooks.Stop[0].matcher' "$hooks_json") + + # This test will FAIL until the bug is fixed + [ "$matcher" = "*" ] +} + +@test "ralph-loop: session-start-hook.sh reads SESSION_ID from stdin" { + local hook="${PLUGIN_DIR}/hooks/session-start-hook.sh" + [ -f "$hook" ] + + # The hook must read from stdin (>.*ENV_FILE' "$hook" + grep -q 'echo.*RALPH_SESSION_ID.*>.*ENV_FILE' "$hook" +} + +# ============================================================================ +# E2E Integration Tests +# These tests verify the core integration scenarios work correctly +# ============================================================================ + +@test "ralph-loop: session-start-hook.sh reads session_id from stdin" { + local hook="${PLUGIN_DIR}/hooks/session-start-hook.sh" + [ -f "$hook" ] + + # Verify the hook reads from stdin using ' "$hook" + grep -q 'PROMISE_TEXT=.*perl' "$hook" +} + +@test "ralph-loop: stop-hook.sh validates numeric iteration fields" { + local hook="${PLUGIN_DIR}/hooks/stop-hook.sh" + [ -f "$hook" ] + + # Verify numeric validation before arithmetic + grep -q 'ITERATION.*0-9' "$hook" + grep -q 'MAX_ITERATIONS.*0-9' "$hook" +} + +@test "ralph-loop: stop-hook.sh handles max iterations reached" { + local hook="${PLUGIN_DIR}/hooks/stop-hook.sh" + [ -f "$hook" ] + + # Verify max iteration check + grep -q 'MAX_ITERATIONS.*-gt.*0.*ITERATION.*-ge.*MAX_ITERATIONS' "$hook" +} + +@test "ralph-loop: cancel-ralph.sh removes state file when cancelled" { + local script="${PLUGIN_DIR}/scripts/cancel-ralph.sh" + [ -f "$script" ] + [ -x "$script" ] + + # Verify state file removal logic + grep -q 'rm.*STATE_FILE' "$script" +}