fix(analyzer): handle Claude CLI JSON array output format
Claude Code CLI outputs a JSON array instead of a single object:
[{type: "system", ...}, {type: "assistant", ...}, {type: "result", ...}]
This caused parse_json_response to fail with "jq: invalid JSON text"
because it assumed the top-level JSON was an object.
Changes:
- Detect if JSON is an array before parsing
- Extract the "result" type message from the array
- Preserve session_id from init message for continuity
- Normalize to object format for existing parsing logic
- Clean up temporary file after processing
Fixes #112
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9b19d70e35
commit
5ce9bc81d7
3 changed files with 204 additions and 4 deletions
16
CLAUDE.md
16
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"* ]]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue