diff --git a/CLAUDE.md b/CLAUDE.md index 0dc6971..e2c7ef6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is the Ralph for Claude Code repository - an autonomous AI development loop system that enables continuous development cycles with intelligent exit detection and rate limiting. -**Version**: v0.10.0 | **Tests**: 310 passing (100% pass rate) | **CI/CD**: GitHub Actions +**Version**: v0.10.1 | **Tests**: 318 passing (100% pass rate) | **CI/CD**: GitHub Actions ## Core Architecture @@ -357,7 +357,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false |------|-------|-------------| | `test_cli_parsing.bats` | 27 | CLI argument parsing for all 12 flags | | `test_cli_modern.bats` | 29 | Modern CLI commands (Phase 1.1) + build_claude_command fix | -| `test_json_parsing.bats` | 36 | JSON output format parsing + Claude CLI format + session management | +| `test_json_parsing.bats` | 44 | JSON output format parsing + Claude CLI format + session management + array format | | `test_session_continuity.bats` | 26 | Session lifecycle management + circuit breaker integration | | `test_exit_detection.bats` | 20 | Exit signal detection | | `test_rate_limiting.bats` | 15 | Rate limiting behavior | @@ -381,6 +381,18 @@ bats tests/unit/test_cli_parsing.bats ## Recent Improvements +### JSON Array Format Support (v0.10.1) +- Fixed `parse_json_response` to handle Claude CLI JSON array output format (issue #112) +- Claude CLI outputs `[{type: "system", ...}, {type: "assistant", ...}, {type: "result", ...}]` +- Previously expected single JSON object, now supports three formats: + 1. Flat format: `{ status, exit_signal, work_type, ... }` + 2. Claude CLI object format: `{ result, sessionId, metadata: {...} }` + 3. Claude CLI array format: `[ {type: "result", ...}, ... ]` +- Extracts `result` type message from array and normalizes to object format +- Preserves `session_id` from init message for session continuity +- Added 8 new tests for JSON array format handling +- Test count: 318 (up from 310) + ### .ralph/ Subfolder Structure (v0.10.0) - BREAKING CHANGE - **Breaking**: Moved all Ralph-specific files to `.ralph/` subfolder - Project root stays clean: only `src/`, `README.md`, and user files remain diff --git a/lib/response_analyzer.sh b/lib/response_analyzer.sh index caf814b..8d81004 100644 --- a/lib/response_analyzer.sh +++ b/lib/response_analyzer.sh @@ -55,12 +55,14 @@ detect_output_format() { # Parse JSON response and extract structured fields # Creates .ralph/.json_parse_result with normalized analysis data -# Supports TWO JSON formats: +# Supports THREE JSON formats: # 1. Flat format: { status, exit_signal, work_type, files_modified, ... } -# 2. Claude CLI format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } } +# 2. Claude CLI object format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } } +# 3. Claude CLI array format: [ {type: "system", ...}, {type: "assistant", ...}, {type: "result", ...} ] parse_json_response() { local output_file=$1 local result_file="${2:-$RALPH_DIR/.json_parse_result}" + local normalized_file="" if [[ ! -f "$output_file" ]]; then echo "ERROR: Output file not found: $output_file" >&2 @@ -73,6 +75,29 @@ parse_json_response() { return 1 fi + # Check if JSON is an array (Claude CLI array format) + # Claude CLI outputs: [{type: "system", ...}, {type: "assistant", ...}, {type: "result", ...}] + if jq -e 'type == "array"' "$output_file" >/dev/null 2>&1; then + normalized_file=$(mktemp) + + # Extract the "result" type message from the array (usually the last entry) + # This contains: result, session_id, is_error, duration_ms, etc. + local result_obj=$(jq '[.[] | select(.type == "result")] | .[-1] // {}' "$output_file" 2>/dev/null) + + # Also extract session_id from init message if not in result object + local init_session_id=$(jq -r '.[] | select(.type == "system" and .subtype == "init") | .session_id // empty' "$output_file" 2>/dev/null | head -1) + + # Build normalized object merging result with session_id + if [[ -n "$init_session_id" && "$init_session_id" != "null" ]]; then + echo "$result_obj" | jq --arg sid "$init_session_id" '. + {sessionId: $sid}' > "$normalized_file" + else + echo "$result_obj" > "$normalized_file" + fi + + # Use normalized file for subsequent parsing + output_file="$normalized_file" + fi + # Detect JSON format by checking for Claude CLI fields local has_result_field=$(jq -r 'has("result")' "$output_file" 2>/dev/null) @@ -194,6 +219,11 @@ parse_json_response() { } }' > "$result_file" + # Cleanup temporary normalized file if created (for array format handling) + if [[ -n "$normalized_file" && -f "$normalized_file" ]]; then + rm -f "$normalized_file" + fi + return 0 } diff --git a/tests/unit/test_json_parsing.bats b/tests/unit/test_json_parsing.bats index 98526ea..601358a 100644 --- a/tests/unit/test_json_parsing.bats +++ b/tests/unit/test_json_parsing.bats @@ -738,3 +738,161 @@ EOF # Should indicate no session to resume [[ "$status" -ne 0 || "$output" == "false" ]] || skip "should_resume_session not yet implemented" } + +# ============================================================================= +# CLAUDE CLI JSON ARRAY FORMAT TESTS (Issue #112) +# ============================================================================= +# Tests for the Claude CLI JSON array output format: +# [ {type: "system", ...}, {type: "assistant", ...}, {type: "result", ...} ] + +@test "detect_output_format identifies JSON array as json" { + local output_file="$LOG_DIR/test_output.log" + + # Create Claude CLI array format output + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-init-123"}, + {"type": "assistant", "message": {"content": [{"type": "text", "text": "Working..."}]}}, + {"type": "result", "subtype": "success", "result": "Task completed", "session_id": "session-result-123"} +] +EOF + + run detect_output_format "$output_file" + assert_equal "$output" "json" +} + +@test "parse_json_response handles Claude CLI JSON array format" { + local output_file="$LOG_DIR/test_output.log" + + # Create Claude CLI array format output (as shown in issue #112) + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "hook_response", "session_id": "session-abc123"}, + {"type": "system", "subtype": "init", "session_id": "session-abc123", "tools": ["Write", "Read"]}, + {"type": "assistant", "message": {"content": [{"type": "text", "text": "Implementing feature..."}]}}, + {"type": "result", "subtype": "success", "result": "All tasks completed successfully.", "session_id": "session-abc123", "is_error": false, "duration_ms": 5000} +] +EOF + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] + + # Should extract result text into summary + local summary=$(jq -r '.summary' "$result_file") + [[ "$summary" == *"All tasks completed"* ]] +} + +@test "parse_json_response extracts session_id from Claude CLI array init message" { + local output_file="$LOG_DIR/test_output.log" + + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-unique-from-init"}, + {"type": "result", "subtype": "success", "result": "Done"} +] +EOF + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] + + local session_id=$(jq -r '.session_id' "$result_file") + assert_equal "$session_id" "session-unique-from-init" +} + +@test "parse_json_response handles empty array gracefully" { + local output_file="$LOG_DIR/test_output.log" + + echo '[]' > "$output_file" + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] + + # Should have default/empty values + local status_val=$(jq -r '.status' "$result_file") + assert_equal "$status_val" "UNKNOWN" +} + +@test "parse_json_response handles array without result type message" { + local output_file="$LOG_DIR/test_output.log" + + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-no-result"}, + {"type": "assistant", "message": {"content": [{"type": "text", "text": "Working..."}]}} +] +EOF + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] + + # Should still work with defaults + local session_id=$(jq -r '.session_id' "$result_file") + assert_equal "$session_id" "session-no-result" +} + +@test "parse_json_response extracts is_error from Claude CLI array result" { + local output_file="$LOG_DIR/test_output.log" + + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-error-test"}, + {"type": "result", "subtype": "error", "result": "Failed to complete", "is_error": true, "duration_ms": 1000} +] +EOF + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] +} + +@test "analyze_response handles Claude CLI JSON array and extracts signals" { + local output_file="$LOG_DIR/test_output.log" + + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-analyze-array"}, + {"type": "assistant", "message": {"content": [{"type": "text", "text": "All work complete."}]}}, + {"type": "result", "subtype": "success", "result": "Project complete and ready for review.", "is_error": false} +] +EOF + + analyze_response "$output_file" 1 + + assert_file_exists "$RALPH_DIR/.response_analysis" + + local output_format=$(jq -r '.output_format' "$RALPH_DIR/.response_analysis") + assert_equal "$output_format" "json" +} + +@test "analyze_response persists session_id from Claude CLI array format" { + local output_file="$LOG_DIR/test_output.log" + + cat > "$output_file" << 'EOF' +[ + {"type": "system", "subtype": "init", "session_id": "session-persist-array-test"}, + {"type": "result", "subtype": "success", "result": "Working on task."} +] +EOF + + analyze_response "$output_file" 1 + + # Session ID should be persisted for continuity + [[ -f "$RALPH_DIR/.claude_session_id" ]] + + local stored_session=$(cat "$RALPH_DIR/.claude_session_id") + [[ "$stored_session" == *"session-persist-array-test"* ]] +}