diff --git a/CLAUDE.md b/CLAUDE.md index e2c7ef6..5b3dd9c 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.1 | **Tests**: 318 passing (100% pass rate) | **CI/CD**: GitHub Actions +**Version**: v0.10.1 | **Tests**: 319 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` | 44 | JSON output format parsing + Claude CLI format + session management + array format | +| `test_json_parsing.bats` | 45 | 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 | @@ -390,8 +390,9 @@ bats tests/unit/test_cli_parsing.bats 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) +- Added 9 new tests for JSON array format handling (including session_id-in-result regression test) +- Review fixes: guard against empty result_obj, prioritize result object's session_id +- Test count: 319 (up from 310) ### .ralph/ Subfolder Structure (v0.10.0) - BREAKING CHANGE - **Breaking**: Moved all Ralph-specific files to `.ralph/` subfolder diff --git a/README.md b/README.md index 4681324..623e070 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![CI](https://github.com/frankbria/ralph-claude-code/actions/workflows/test.yml/badge.svg)](https://github.com/frankbria/ralph-claude-code/actions/workflows/test.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -![Version](https://img.shields.io/badge/version-0.10.0-blue) -![Tests](https://img.shields.io/badge/tests-308%20passing-green) +![Version](https://img.shields.io/badge/version-0.10.1-blue) +![Tests](https://img.shields.io/badge/tests-310%20passing-green) [![GitHub Issues](https://img.shields.io/github/issues/frankbria/ralph-claude-code)](https://github.com/frankbria/ralph-claude-code/issues) [![Mentioned in Awesome Claude Code](https://awesome.re/mentioned-badge.svg)](https://github.com/hesreallyhim/awesome-claude-code) [![Follow on X](https://img.shields.io/twitter/follow/FrankBria18044?style=social)](https://x.com/FrankBria18044) @@ -16,9 +16,9 @@ Ralph is an implementation of the Geoffrey Huntley's technique for Claude Code t ## Project Status -**Version**: v0.10.0 - Active Development +**Version**: v0.10.1 - Active Development **Core Features**: Working and tested -**Test Coverage**: 308 tests, 100% pass rate +**Test Coverage**: 310 tests, 100% pass rate ### What's Working Now - Autonomous development loops with intelligent exit detection @@ -36,10 +36,26 @@ Ralph is an implementation of the Geoffrey Huntley's technique for Claude Code t - PRD import functionality - **CI/CD pipeline with GitHub Actions** - **Dedicated uninstall script for clean removal** -- 308 passing tests across 11 test files +- 310 passing tests across 11 test files ### Recent Improvements +**v0.10.1 - Bug Fixes & Monitor Path Corrections** +- Fixed `ralph_monitor.sh` hardcoded paths for v0.10.0 compatibility: + - `STATUS_FILE`: `status.json` → `.ralph/status.json` + - `LOG_FILE`: `logs/ralph.log` → `.ralph/logs/ralph.log` + - `progress.json` → `.ralph/progress.json` +- Fixed EXIT_SIGNAL parsing in JSON format (Bug #1): + - Now extracts `EXIT_SIGNAL` from `.result` field when Claude CLI returns JSON + - Properly detects RALPH_STATUS blocks embedded in JSON response text +- Added safety circuit breaker (Bug #2): + - Force exit after 5 consecutive completion indicators (prevents infinite loops) + - Higher threshold than normal (2) to avoid false positives while preventing API waste +- Fixed checkbox parsing for indented markdown (Bug #3): + - Changed patterns from `^- \[` to `^[[:space:]]*- \[` (POSIX-compliant) + - Supports indented checkboxes in `@fix_plan.md` +- Updated README.md documentation example for new log path + **v0.10.0 - .ralph/ Subfolder Structure (BREAKING CHANGE)** - **Breaking**: Moved all Ralph-specific files to `.ralph/` subfolder - Project root stays clean: only `src/`, `README.md`, and user files remain @@ -584,7 +600,7 @@ Shows real-time: ralph --status # Manual log inspection -tail -f logs/ralph.log +tail -f .ralph/logs/ralph.log ``` ### Common Issues diff --git a/create_files.sh b/create_files.sh index 26ca9fd..4028c55 100755 --- a/create_files.sh +++ b/create_files.sh @@ -204,9 +204,10 @@ should_exit_gracefully() { fi # 4. Check fix_plan.md for completion + # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then - local total_items=$(grep -c "^- \[" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") - local completed_items=$(grep -c "^- \[x\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") + local total_items=$(grep -cE "^[[:space:]]*- \[" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") + local completed_items=$(grep -cE "^[[:space:]]*- \[x\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then log_status "WARN" "Exit condition: All fix_plan.md items completed ($completed_items/$total_items)" diff --git a/lib/response_analyzer.sh b/lib/response_analyzer.sh index 8d81004..d3d3cf3 100644 --- a/lib/response_analyzer.sh +++ b/lib/response_analyzer.sh @@ -84,14 +84,25 @@ parse_json_response() { # 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 + # Guard against empty result_obj if jq fails (review fix: Macroscope) + [[ -z "$result_obj" ]] && result_obj="{}" + + # Extract session_id from init message as fallback 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" + # Prioritize result object's own session_id, then fall back to init message (review fix: CodeRabbit) + # This prevents session ID loss when arrays lack an init message with session_id + local effective_session_id + effective_session_id=$(echo "$result_obj" | jq -r '.sessionId // .session_id // empty' 2>/dev/null) + if [[ -z "$effective_session_id" || "$effective_session_id" == "null" ]]; then + effective_session_id="$init_session_id" + fi + + # Build normalized object merging result with effective session_id + if [[ -n "$effective_session_id" && "$effective_session_id" != "null" ]]; then + echo "$result_obj" | jq --arg sid "$effective_session_id" '. + {sessionId: $sid} | del(.session_id)' > "$normalized_file" else - echo "$result_obj" > "$normalized_file" + echo "$result_obj" | jq 'del(.session_id)' > "$normalized_file" fi # Use normalized file for subsequent parsing @@ -114,6 +125,27 @@ parse_json_response() { # Exit signal: from flat format OR derived from completion_status local exit_signal=$(jq -r '.exit_signal // false' "$output_file" 2>/dev/null) + # Bug #1 Fix: If exit_signal is still false, check for RALPH_STATUS block in .result field + # Claude CLI JSON format embeds the RALPH_STATUS block within the .result text field + if [[ "$exit_signal" == "false" && "$has_result_field" == "true" ]]; then + local result_text=$(jq -r '.result // ""' "$output_file" 2>/dev/null) + if [[ -n "$result_text" ]] && echo "$result_text" | grep -q -- "---RALPH_STATUS---"; then + # Extract EXIT_SIGNAL value from RALPH_STATUS block within result text + local embedded_exit_sig=$(echo "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2 | xargs) + if [[ "$embedded_exit_sig" == "true" ]]; then + exit_signal="true" + [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Extracted EXIT_SIGNAL=true from .result RALPH_STATUS block" >&2 + fi + # Also check STATUS field as fallback + local embedded_status=$(echo "$result_text" | grep "STATUS:" | cut -d: -f2 | xargs) + if [[ "$embedded_status" == "COMPLETE" && "$exit_signal" != "true" ]]; then + # STATUS: COMPLETE without explicit EXIT_SIGNAL implies completion + exit_signal="true" + [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Inferred EXIT_SIGNAL=true from .result STATUS=COMPLETE" >&2 + fi + fi + fi + # Work type: from flat format local work_type=$(jq -r '.work_type // "UNKNOWN"' "$output_file" 2>/dev/null) diff --git a/ralph_loop.sh b/ralph_loop.sh index 3e48d38..bff890f 100755 --- a/ralph_loop.sh +++ b/ralph_loop.sh @@ -313,7 +313,17 @@ should_exit_gracefully() { return 0 fi - # 3. Strong completion indicators (only if Claude's EXIT_SIGNAL is true) + # 3. Safety circuit breaker - force exit after 5 consecutive completion indicators + # Bug #2 Fix: Prevents infinite loops when EXIT_SIGNAL is not explicitly set + # but completion patterns clearly indicate work is done. Threshold of 5 is higher + # than normal threshold (2) to avoid false positives while preventing API waste. + if [[ $recent_completion_indicators -ge 5 ]]; then + log_status "WARN" "🚨 SAFETY CIRCUIT BREAKER: Force exit after 5 consecutive completion indicators ($recent_completion_indicators)" >&2 + echo "safety_circuit_breaker" + return 0 + fi + + # 4. Strong completion indicators (only if Claude's EXIT_SIGNAL is true) # This prevents premature exits when heuristics detect completion patterns # but Claude explicitly indicates work is still in progress via RALPH_STATUS block. # The exit_signal in .response_analysis represents Claude's explicit intent. @@ -330,10 +340,11 @@ should_exit_gracefully() { log_status "INFO" "DEBUG: Completion indicators ($recent_completion_indicators) present but EXIT_SIGNAL=false, continuing..." >&2 fi - # 4. Check fix_plan.md for completion + # 5. Check fix_plan.md for completion + # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then - local total_items=$(grep -c "^- \[" "$RALPH_DIR/@fix_plan.md" 2>/dev/null) - local completed_items=$(grep -c "^- \[x\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null) + local total_items=$(grep -cE "^[[:space:]]*- \[" "$RALPH_DIR/@fix_plan.md" 2>/dev/null) + local completed_items=$(grep -cE "^[[:space:]]*- \[x\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null) # Handle case where grep returns no matches (exit code 1) [[ -z "$total_items" ]] && total_items=0 @@ -445,8 +456,9 @@ build_loop_context() { context="Loop #${loop_count}. " # Extract incomplete tasks from @fix_plan.md + # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then - local incomplete_tasks=$(grep -c "^- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") + local incomplete_tasks=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0") context+="Remaining tasks: ${incomplete_tasks}. " fi diff --git a/ralph_monitor.sh b/ralph_monitor.sh index 767fcb2..3bf76cc 100755 --- a/ralph_monitor.sh +++ b/ralph_monitor.sh @@ -3,8 +3,8 @@ # Ralph Status Monitor - Live terminal dashboard for the Ralph loop set -e -STATUS_FILE="status.json" -LOG_FILE="logs/ralph.log" +STATUS_FILE=".ralph/status.json" +LOG_FILE=".ralph/logs/ralph.log" REFRESH_INTERVAL=2 # Colors @@ -74,8 +74,8 @@ display_status() { fi # Claude Code Progress section - if [[ -f "progress.json" ]]; then - local progress_data=$(cat "progress.json" 2>/dev/null) + if [[ -f ".ralph/progress.json" ]]; then + local progress_data=$(cat ".ralph/progress.json" 2>/dev/null) local progress_status=$(echo "$progress_data" | jq -r '.status // "idle"' 2>/dev/null || echo "idle") if [[ "$progress_status" == "executing" ]]; then diff --git a/tests/unit/test_json_parsing.bats b/tests/unit/test_json_parsing.bats index 601358a..178446a 100644 --- a/tests/unit/test_json_parsing.bats +++ b/tests/unit/test_json_parsing.bats @@ -896,3 +896,26 @@ EOF local stored_session=$(cat "$RALPH_DIR/.claude_session_id") [[ "$stored_session" == *"session-persist-array-test"* ]] } + +# Regression test: arrays where only result element carries session_id (review fix: CodeRabbit) +@test "parse_json_response extracts session_id from result object when no init message" { + local output_file="$LOG_DIR/test_output.log" + + # Array with session_id only in result object, no init message + cat > "$output_file" << 'EOF' +[ + {"type": "assistant", "message": {"content": [{"type": "text", "text": "Working..."}]}}, + {"type": "result", "subtype": "success", "result": "Task complete.", "session_id": "session-in-result-only"} +] +EOF + + run parse_json_response "$output_file" + assert_equal "$status" "0" + + local result_file="$RALPH_DIR/.json_parse_result" + [[ -f "$result_file" ]] + + # Session ID should be extracted from result object + local session_id=$(jq -r '.session_id' "$result_file") + assert_equal "$session_id" "session-in-result-only" +}