diff --git a/ralph_loop.sh b/ralph_loop.sh index c243052..683d654 100755 --- a/ralph_loop.sh +++ b/ralph_loop.sh @@ -1147,9 +1147,11 @@ execute_claude_code() { # Use stdbuf to disable buffering for real-time output # Use portable_timeout for consistent timeout protection (Issue: missing timeout) # Capture all pipeline exit codes for proper error handling + # stdin must be redirected from /dev/null because newer Claude CLI versions + # read from stdin even in -p (print) mode, causing the process to hang set -o pipefail portable_timeout ${timeout_seconds}s stdbuf -oL "${LIVE_CMD_ARGS[@]}" \ - 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE" + < /dev/null 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE" # Capture exit codes from pipeline local -a pipe_status=("${PIPESTATUS[@]}") @@ -1206,7 +1208,10 @@ execute_claude_code() { if [[ "$use_modern_cli" == "true" ]]; then # Modern execution with command array (shell-injection safe) # Execute array directly without bash -c to prevent shell metacharacter interpretation - if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" > "$output_file" 2>&1 & + # stdin must be redirected from /dev/null because newer Claude CLI versions + # read from stdin even in -p (print) mode, causing SIGTTIN suspension + # when the process is backgrounded + if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>&1 & then : # Continue to wait loop else diff --git a/tests/unit/test_cli_modern.bats b/tests/unit/test_cli_modern.bats index 6bdbfe7..28dbfbe 100644 --- a/tests/unit/test_cli_modern.bats +++ b/tests/unit/test_cli_modern.bats @@ -642,6 +642,71 @@ EOF [[ "$found_prompt" == "true" ]] } +# ============================================================================= +# BACKGROUND EXECUTION STDIN REDIRECT TESTS +# Newer Claude CLI reads stdin even in -p mode, causing SIGTTIN suspension +# when the process is backgrounded. Verify /dev/null redirect is present. +# ============================================================================= + +@test "modern CLI background execution redirects stdin from /dev/null" { + # Verify the implementation in ralph_loop.sh redirects stdin from /dev/null + # to prevent SIGTTIN suspension when claude is backgrounded. + # Without this, newer Claude CLI versions hang indefinitely. + + run grep 'portable_timeout.*CLAUDE_CMD_ARGS.*< /dev/null.*&' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" + + assert_success + [[ "$output" == *'< /dev/null'* ]] +} + +@test "live mode execution redirects stdin from /dev/null" { + # Verify the live (streaming) mode also redirects stdin from /dev/null. + # This path is used by ralph --monitor (which adds --live). + # The live mode splits across two lines (line continuation with \), + # so we check the continuation line that has < /dev/null. + + local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh" + + # The live mode has LIVE_CMD_ARGS on one line and < /dev/null on the next + run grep '< /dev/null 2>&1 |' "$script" + + assert_success + [[ "$output" == *'< /dev/null'* ]] +} + +@test "all claude execution paths redirect stdin" { + # Verify that ALL portable_timeout invocations of claude redirect stdin, + # to prevent regressions. There are 3 paths: modern background, live, legacy. + # Legacy uses < "$PROMPT_FILE", the other two must use < /dev/null. + # We check that no portable_timeout line invoking claude lacks a stdin redirect + # (either on the same line or a continuation line). + + local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh" + + # All 3 portable_timeout lines that invoke claude should have < somewhere nearby + # Modern background: has < /dev/null on same line + run grep 'portable_timeout.*CLAUDE_CMD_ARGS.*< /dev/null' "$script" + assert_success + + # Live mode: has < /dev/null on continuation line + run grep '< /dev/null 2>&1 |' "$script" + assert_success + + # Legacy mode: has < "$PROMPT_FILE" on same line + run grep 'portable_timeout.*CLAUDE_CODE_CMD.*< ' "$script" + assert_success +} + +@test "modern CLI background execution has comment explaining stdin redirect" { + # Verify the fix is documented with context about why /dev/null is needed + + run grep -c 'stdin must be redirected' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh" + + assert_success + # Should appear in both background and live mode sections + [[ "$output" == "2" ]] +} + # ============================================================================= # .RALPHRC CONFIGURATION LOADING TESTS # Tests for the environment variable precedence fix