fix(live-mode): crash with text output format (#164)

Live/monitor mode crashed with `stdbuf: unrecognized option '--verbose'`
when CLAUDE_OUTPUT_FORMAT="text" because build_claude_command() was only
called inside a JSON-only gate, leaving CLAUDE_CMD_ARGS empty.

Three coordinated fixes:
- Override text→json when live mode is active (stream-json requires JSON)
- Always call build_claude_command() regardless of output format
- Add safety check for empty CLAUDE_CMD_ARGS before live mode construction

Also updates help text for --live and --output-format to document the
auto-switch behavior.
This commit is contained in:
Test User 2026-02-07 11:03:39 -07:00
parent 204761fa8e
commit 4027ac929a
2 changed files with 115 additions and 12 deletions

View file

@ -1048,21 +1048,20 @@ execute_claude_code() {
session_id=$(init_claude_session)
fi
# Live mode requires JSON output (stream-json) — override text format
if [[ "$LIVE_OUTPUT" == "true" && "$CLAUDE_OUTPUT_FORMAT" == "text" ]]; then
log_status "WARN" "Live mode requires JSON output format. Overriding text → json for this session."
CLAUDE_OUTPUT_FORMAT="json"
fi
# Build the Claude CLI command with modern flags
# Note: We use the modern CLI with -p flag when CLAUDE_OUTPUT_FORMAT is "json"
# For backward compatibility, fall back to stdin piping for text mode
local use_modern_cli=false
if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
# Modern approach: use CLI flags (builds CLAUDE_CMD_ARGS array)
if build_claude_command "$PROMPT_FILE" "$loop_context" "$session_id"; then
use_modern_cli=true
log_status "INFO" "Using modern CLI mode (JSON output)"
else
log_status "WARN" "Failed to build modern CLI command, falling back to legacy mode"
fi
if build_claude_command "$PROMPT_FILE" "$loop_context" "$session_id"; then
use_modern_cli=true
log_status "INFO" "Using modern CLI mode (${CLAUDE_OUTPUT_FORMAT} output)"
else
log_status "INFO" "Using legacy CLI mode (text output)"
log_status "WARN" "Failed to build modern CLI command, falling back to legacy mode"
fi
# Execute Claude Code
@ -1091,6 +1090,14 @@ execute_claude_code() {
fi
fi
if [[ "$LIVE_OUTPUT" == "true" ]]; then
# Safety check: CLAUDE_CMD_ARGS must be populated for live mode
if [[ ${#CLAUDE_CMD_ARGS[@]} -eq 0 ]]; then
log_status "ERROR" "CLAUDE_CMD_ARGS is empty — cannot build live mode command. Falling back to background mode."
LIVE_OUTPUT=false
fi
fi
if [[ "$LIVE_OUTPUT" == "true" ]]; then
log_status "INFO" "📺 Live output mode enabled - showing Claude Code streaming..."
echo -e "${PURPLE}━━━━━━━━━━━━━━━━ Claude Code Output ━━━━━━━━━━━━━━━━${NC}"
@ -1613,7 +1620,7 @@ Options:
-s, --status Show current status and exit
-m, --monitor Start with tmux session and live monitor (requires tmux)
-v, --verbose Show detailed progress updates during execution
-l, --live Show Claude Code output in real-time (streaming mode)
-l, --live Show Claude Code output in real-time (auto-switches to JSON output)
-t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
--reset-circuit Reset circuit breaker to CLOSED state
--circuit-status Show circuit breaker status and exit
@ -1622,6 +1629,7 @@ Options:
Modern CLI Options (Phase 1.1):
--output-format FORMAT Set Claude output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
Note: --live mode requires JSON and will auto-switch
--allowed-tools TOOLS Comma-separated list of allowed tools (default: $CLAUDE_ALLOWED_TOOLS)
--no-continue Disable session continuity across loops
--session-expiry HOURS Set session expiration time in hours (default: $CLAUDE_SESSION_EXPIRY_HOURS)

View file

@ -667,3 +667,98 @@ EOF
[[ "$output" == *'_env_MAX_CALLS_PER_HOUR'* ]]
[[ "$output" == *'_env_CLAUDE_TIMEOUT_MINUTES'* ]]
}
# =============================================================================
# LIVE MODE + TEXT FORMAT FIX TESTS (Issue #164)
# Tests for: live mode format override, always-call build_claude_command,
# and safety check for empty CLAUDE_CMD_ARGS
# =============================================================================
@test "build_claude_command works for text format (populates CLAUDE_CMD_ARGS)" {
export CLAUDE_CODE_CMD="claude"
export CLAUDE_OUTPUT_FORMAT="text"
export CLAUDE_ALLOWED_TOOLS="Write,Read"
export CLAUDE_USE_CONTINUE="false"
echo "Test prompt content" > "$PROMPT_FILE"
build_claude_command "$PROMPT_FILE" "" ""
# CLAUDE_CMD_ARGS should be populated even in text mode
[[ ${#CLAUDE_CMD_ARGS[@]} -gt 0 ]]
local cmd_string="${CLAUDE_CMD_ARGS[*]}"
# Should contain claude command and -p flag
[[ "$cmd_string" == *"claude"* ]]
[[ "$cmd_string" == *"-p"* ]]
[[ "$cmd_string" == *"Test prompt content"* ]]
# Should NOT contain --output-format (text mode omits it)
[[ "$cmd_string" != *"--output-format"* ]]
# Should still include allowed tools
[[ "$cmd_string" == *"--allowedTools"* ]]
[[ "$cmd_string" == *"Write"* ]]
}
@test "build_claude_command works for json format (includes --output-format json)" {
export CLAUDE_CODE_CMD="claude"
export CLAUDE_OUTPUT_FORMAT="json"
export CLAUDE_ALLOWED_TOOLS=""
export CLAUDE_USE_CONTINUE="false"
echo "Test prompt" > "$PROMPT_FILE"
build_claude_command "$PROMPT_FILE" "" ""
local cmd_string="${CLAUDE_CMD_ARGS[*]}"
# Should contain --output-format json
[[ "$cmd_string" == *"--output-format"* ]]
[[ "$cmd_string" == *"json"* ]]
[[ "$cmd_string" == *"-p"* ]]
}
@test "live mode overrides text to json format in ralph_loop.sh" {
# Verify ralph_loop.sh contains the live mode format override logic
run grep -A3 'LIVE_OUTPUT.*true.*CLAUDE_OUTPUT_FORMAT.*text' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Should find the override block
[[ "$output" == *"CLAUDE_OUTPUT_FORMAT"* ]]
[[ "$output" == *"json"* ]]
}
@test "live mode format override preserves json format unchanged" {
# The override should only trigger when format is "text", not "json"
# Verify the condition checks for text specifically
run grep 'CLAUDE_OUTPUT_FORMAT.*text' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Should check specifically for "text" (not a blanket override)
[[ "$output" == *'"text"'* ]]
}
@test "safety check prevents live mode with empty CLAUDE_CMD_ARGS" {
# Verify ralph_loop.sh has the safety check for empty CLAUDE_CMD_ARGS
run grep -A3 'CLAUDE_CMD_ARGS.*-eq 0' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Should find safety check that falls back to background mode
[[ "$output" == *"LIVE_OUTPUT"* ]] || [[ "$output" == *"empty"* ]] || [[ "$output" == *"background"* ]]
}
@test "build_claude_command is called regardless of output format in ralph_loop.sh" {
# Verify that build_claude_command is NOT gated behind JSON-only check
# The old pattern was: if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then build_claude_command...
# The new pattern should call build_claude_command unconditionally
# Check that build_claude_command call is NOT inside a JSON-only conditional
# Look for the actual call site (not the function definition or comments)
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# The old pattern: "json" check immediately followed by build_claude_command
# should no longer exist as a gate
run bash -c "sed -n '/# Build the Claude CLI command/,/execute Claude Code/p' '$script' | grep -c 'CLAUDE_OUTPUT_FORMAT.*json.*build_claude_command'"
# Should find 0 matches (the gate has been removed)
[[ "$output" == "0" ]]
}