Skip to content

fix(loop): guard bare function calls against set -e script termination#207

Closed
timothy-20 wants to merge 40 commits intofrankbria:mainfrom
timothy-20:fix/set-e-bare-calls-190
Closed

fix(loop): guard bare function calls against set -e script termination#207
timothy-20 wants to merge 40 commits intofrankbria:mainfrom
timothy-20:fix/set-e-bare-calls-190

Conversation

@timothy-20
Copy link

@timothy-20 timothy-20 commented Feb 28, 2026

Closed: Superseded by #208 (refactor/remove-set-e), which removes set -e entirely — making the bare call guards in this PR unnecessary.

timothy-20 and others added 30 commits February 27, 2026 02:08
…rankbria#190)

build_loop_context() was only called inside the CLAUDE_USE_CONTINUE=true
conditional, so SESSION_CONTINUITY=false sessions received no loop context
(loop number, remaining tasks, circuit breaker state, previous summary).
This is especially problematic for fresh sessions that lack prior context.

Move the call outside the conditional so context is always built and passed
via --append-system-prompt. Session ID initialization remains gated behind
CLAUDE_USE_CONTINUE as intended.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nkbria#190)

Add QUESTION_PATTERNS array and detect_questions() function to identify
when Claude is asking questions instead of acting autonomously. Integrate
detection into both JSON and text analysis paths, emitting
asking_questions (bool) and question_count (int) fields in
.response_analysis output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rankbria#190)

When Claude asks questions instead of acting, hold consecutive_no_progress
steady rather than incrementing. This prevents the circuit breaker from
opening prematurely during question loops in headless mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ons (frankbria#190)

When .response_analysis shows asking_questions=true, build_loop_context()
now appends a headless-mode corrective message telling Claude to choose
safe defaults and proceed autonomously instead of asking questions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… detect_questions

- test_json_parsing: 5 tests for detect_questions() (pattern+?, no pattern,
  no ?, empty input, multiple questions)
- test_exit_detection: 2 tests for analyze_response() asking_questions field
- test_cli_modern: 2 tests for build_loop_context() corrective message +
  sync inline function copy with ralph_loop.sh
- Fix: sanitize grep -c output with tr -d before arithmetic evaluation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add asking_questions and question_count to structured fields list
- Document detect_questions() and QUESTION_PATTERNS in response_analyzer
- Add question loop suppression to circuit breaker thresholds section
- Add corrective guidance to loop context documentation
- Update test counts: 568 → 578 (json_parsing 57, exit_detection 55, cli_modern 71)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…estion detection (frankbria#190)

work_summary is already populated from .json_parse_result at L372.
The intermediate json_text_content variable and file re-read were
unnecessary defensive code that duplicated the same jq extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix(frankbria#190): detect question loops and suppress circuit breaker in headless mode

3-layer solution for Bug 2 — SESSION_CONTINUITY=false headless question loop:
1. Question pattern detection (detect_questions + QUESTION_PATTERNS)
2. Circuit breaker no-progress counter freeze on question loops
3. Corrective guidance injection in next loop iteration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ng (frankbria#190)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
check_claude_version() was defined but never called in the startup
sequence. Add both check_claude_version() and new check_claude_updates()
calls after validate_claude_command(), ensuring CLI compatibility and
freshness before entering the loop. Auto-update queries npm registry
and attempts upgrade; failures are non-blocking (WARN only).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rankbria#190)

- Add compare_semver() helper with proper major→minor→patch sequential
  comparison, replacing integer arithmetic (major*10000+minor*100+patch)
  that produced incorrect results for patch numbers >= 100
- Refactor check_claude_version() and check_claude_updates() to use
  compare_semver(), eliminating duplicated version comparison logic
- Add CLAUDE_AUTO_UPDATE config flag (default: true) to .ralphrc,
  templates, enable_core, and setup — allows disabling auto-update
  checks for air-gapped environments
- Add 7 new tests: semver edge cases + CLAUDE_AUTO_UPDATE behavior
- Update CLAUDE.md: test counts 588→595, document new config variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nkbria#190)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…verage (frankbria#190)

Remove 49 redundant tests: cross-file duplicates (6), structural grep-only
tests covered by behavioral tests (26), and threshold/parameter duplicates
consolidated into single tests (17). 595 -> 546 tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3 test files hardcoded /tmp/ralph-* paths in mktemp, causing
'Operation not permitted' in sandboxed environments. Bare mktemp -d
uses the system default temp directory, consistent with the other
15 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n-UTC systems

The BSD date fallback in get_past_timestamp() used date -r which outputs
local time, but the format string hardcoded +00:00 (UTC). On non-UTC systems
(e.g. JST+9), parse_iso_to_epoch() interpreted the local time as UTC,
producing a future epoch and negative elapsed minutes, causing 5 cooldown
tests to always fail.

Adding -u flag to date -r ensures UTC output matching the +00:00 suffix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…paths

- Kill orphaned Claude child process on Ctrl+C via global _CLAUDE_PID tracking
- Switch from trap SIGINT/SIGTERM to trap EXIT to cover set -e and normal exits
- Add reentrancy guard (_CLEANUP_DONE) to prevent double cleanup execution
- Reset stale .exit_signals and .response_analysis on startup (SIGKILL/OOM recovery)
- Clean up empty stderr files and temporary stream logs to prevent accumulation
- Fix test_session_continuity grep range to verify reset_session in expanded cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rankbria#190)

build_loop_context() was only called inside the CLAUDE_USE_CONTINUE=true
conditional, so SESSION_CONTINUITY=false sessions received no loop context
(loop number, remaining tasks, circuit breaker state, previous summary).
This is especially problematic for fresh sessions that lack prior context.

Move the call outside the conditional so context is always built and passed
via --append-system-prompt. Session ID initialization remains gated behind
CLAUDE_USE_CONTINUE as intended.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nkbria#190)

Add QUESTION_PATTERNS array and detect_questions() function to identify
when Claude is asking questions instead of acting autonomously. Integrate
detection into both JSON and text analysis paths, emitting
asking_questions (bool) and question_count (int) fields in
.response_analysis output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rankbria#190)

When Claude asks questions instead of acting, hold consecutive_no_progress
steady rather than incrementing. This prevents the circuit breaker from
opening prematurely during question loops in headless mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ons (frankbria#190)

When .response_analysis shows asking_questions=true, build_loop_context()
now appends a headless-mode corrective message telling Claude to choose
safe defaults and proceed autonomously instead of asking questions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… detect_questions

- test_json_parsing: 5 tests for detect_questions() (pattern+?, no pattern,
  no ?, empty input, multiple questions)
- test_exit_detection: 2 tests for analyze_response() asking_questions field
- test_cli_modern: 2 tests for build_loop_context() corrective message +
  sync inline function copy with ralph_loop.sh
- Fix: sanitize grep -c output with tr -d before arithmetic evaluation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add asking_questions and question_count to structured fields list
- Document detect_questions() and QUESTION_PATTERNS in response_analyzer
- Add question loop suppression to circuit breaker thresholds section
- Add corrective guidance to loop context documentation
- Update test counts: 568 → 578 (json_parsing 57, exit_detection 55, cli_modern 71)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…estion detection (frankbria#190)

work_summary is already populated from .json_parse_result at L372.
The intermediate json_text_content variable and file re-read were
unnecessary defensive code that duplicated the same jq extraction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ng (frankbria#190)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
timothy-20 and others added 9 commits February 27, 2026 19:30
check_claude_version() was defined but never called in the startup
sequence. Add both check_claude_version() and new check_claude_updates()
calls after validate_claude_command(), ensuring CLI compatibility and
freshness before entering the loop. Auto-update queries npm registry
and attempts upgrade; failures are non-blocking (WARN only).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rankbria#190)

- Add compare_semver() helper with proper major→minor→patch sequential
  comparison, replacing integer arithmetic (major*10000+minor*100+patch)
  that produced incorrect results for patch numbers >= 100
- Refactor check_claude_version() and check_claude_updates() to use
  compare_semver(), eliminating duplicated version comparison logic
- Add CLAUDE_AUTO_UPDATE config flag (default: true) to .ralphrc,
  templates, enable_core, and setup — allows disabling auto-update
  checks for air-gapped environments
- Add 7 new tests: semver edge cases + CLAUDE_AUTO_UPDATE behavior
- Update CLAUDE.md: test counts 588→595, document new config variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nkbria#190)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…verage (frankbria#190)

Remove 49 redundant tests: cross-file duplicates (6), structural grep-only
tests covered by behavioral tests (26), and threshold/parameter duplicates
consolidated into single tests (17). 595 -> 546 tests, all passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
….ralphrc (frankbria#190)

- Wrap detect_questions() calls in if-guards to prevent set -e from
  aborting on exit code 1 (no questions found)
- Add chmod 600 to .ralphrc in setup.sh for both lib and fallback paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e patterns (frankbria#190)

Remove the ? bail-out and ? filter in detect_questions() so that
declarative wait patterns (e.g. "Please confirm", "Awaiting input")
are detected without requiring a question mark. Use grep -ciw for
word-boundary matching to prevent false positives from substring
overlap (e.g. "should implement" no longer matches "should I").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	lib/response_analyzer.sh
#	tests/unit/test_json_parsing.bats
frankbria#190)

set -e (line 6) converts non-zero return from bare function calls into
immediate script exit, making most error handling code dead code.

Fixes 7 instances:
- check_claude_version: return 1 -> return 0 (non-blocking warning)
- check_claude_updates: return 1 -> return 0 (non-blocking warning)
- execute_claude_code: bare call -> || exec_result=$?
- record_loop_result: bare call -> || circuit_result=$?
- analyze_response: bare call -> || analysis_exit_code=$?
- update_exit_signals: bare call -> || true
- log_analysis_summary: bare call -> || true

The || pattern exploits bash rule that set -e is disabled in LHS of ||,
safely capturing exit codes without script termination.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Walkthrough

The PR adds question-detection to response analysis, surfaces asking_questions/question_count, integrates that signal into the circuit-breaker to suppress no-progress increments, adds semantic version checks and an optional auto-update flow for the Claude CLI, redirects live-mode stderr to per-run logs, and updates tests and templates to exercise these behaviors.

Changes

Cohort / File(s) Summary
Response analysis & question detection
lib/response_analyzer.sh
Added QUESTION_PATTERNS and detect_questions(); analysis now emits asking_questions and question_count for both JSON and text paths; detect_questions exported.
Circuit breaker progress handling
lib/circuit_breaker.sh
record_loop_result() now reads asking_questions and treats question-asking as non-progress without incrementing stagnation counters; log_circuit_transition() writes fallback single-entry history if jq fails.
Startup, versioning & auto-update
ralph_loop.sh, lib/enable_core.sh, setup.sh, templates/ralphrc.template
Added CLAUDE_AUTO_UPDATE flag (default true), compare_semver(), check_claude_version(), check_claude_updates(); startup now validates version and optionally attempts npm-based update (non-blocking on failure); propagated config into .ralphrc template and setup.
Process, stderr & lifecycle
ralph_loop.sh
Redirects Claude CLI stderr to per-execution stderr files in live and non-live modes, introduces _CLAUDE_PID and _CLEANUP_DONE guards, centralizes cleanup/trap handling, and ensures loop context always built (including corrective messaging when prior loop asked questions).
Documentation / Manifest
CLAUDE.md
Documented new analysis fields, question-detection behavior, version-check/update flow, live-mode stderr handling, and loop suppression behavior.
Tests (restructure & additions)
tests/unit/... (test_cli_modern.bats, test_json_parsing.bats, test_circuit_breaker_recovery.bats, test_exit_detection.bats, test_session_continuity.bats, test_file_protection.bats, test_integrity_check.bats, test_rate_limiting.bats)
Added extensive question-detection and semver tests; updated live-mode stderr tests; removed/consolidated many session, saturation, and optional-file tests; adjusted mktemp/date usage for portability; added resilience test for corrupted history file. Overall test counts changed.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant ralph as ralph_loop.sh
  participant npm as npm_registry
  participant claude as Claude_CLI
  participant analyzer as response_analyzer.sh
  participant cb as circuit_breaker.sh

  User->>ralph: start CLI (startup)
  ralph->>ralph: validate command
  ralph->>ralph: check_claude_version() / compare_semver()
  alt update enabled
    ralph->>npm: check_claude_updates()
    npm-->>ralph: latest_version
    alt newer available
      ralph->>npm: attempt npm update (non-blocking)
      npm-->>ralph: update result
    end
  end
  ralph->>claude: launch Claude CLI (capture stderr to file)
  claude-->>ralph: stdout (JSON/text)
  claude-->>ralph: stderr (to stderr file)
  ralph->>analyzer: analyze_response(stdout)
  analyzer-->>ralph: analysis JSON (incl. asking_questions, question_count)
  ralph->>cb: record_loop_result(analysis)
  cb-->>ralph: update circuit state (suppress no-progress if asking_questions)
  ralph->>User: loop continues or circuit triggers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I sniff the prompts and count each plea,
Questions detected, one-two-three.
Versions checked, stderr tucked away,
Loops keep hopping, safe today—
A little rabbit cheers: code’s on its way! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing script termination issues caused by set -e when encountering non-zero function returns in bare calls within ralph_loop.sh.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/unit/test_file_protection.bats (1)

187-194: Verify all optional files are excluded, not just one.

The comment at line 41 lists four optional files (.ralph/logs/, .ralph/status.json, .ralph/.call_count, .ralph/.exit_signals), but line 193 only verifies that status.json is not in RALPH_REQUIRED_PATHS. Consider checking all optional files for comprehensive coverage.

🔍 Proposed fix to verify all optional paths
     # Optional paths should NOT be required
-    [[ ! " ${RALPH_REQUIRED_PATHS[*]} " =~ "status.json" ]]
+    local optional=(".ralph/logs" "status.json" ".call_count" ".exit_signals")
+    for opt_path in "${optional[@]}"; do
+        [[ ! " ${RALPH_REQUIRED_PATHS[*]} " =~ "$opt_path" ]]
+    done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_file_protection.bats` around lines 187 - 194, The test
"RALPH_REQUIRED_PATHS contains all critical paths and excludes optional files"
currently only asserts that "status.json" is not required; update the test to
also assert that ".ralph/logs/", ".ralph/.call_count", and
".ralph/.exit_signals" are not present in RALPH_REQUIRED_PATHS. Locate the test
block (function name or description) and add negative membership checks for each
optional path against the RALPH_REQUIRED_PATHS array so all four optional files
listed in the comment are verified as excluded.
ralph_loop.sh (1)

1490-1491: analysis_exit_code is captured but never used.

This adds dead state without affecting behavior. Either remove it or log/action it when non-zero.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ralph_loop.sh` around lines 1490 - 1491, The variable analysis_exit_code is
assigned but never used after calling analyze_response("$output_file"
"$loop_count"), so either remove the unused capture or handle non-zero results;
modify the call in ralph_loop.sh to either just call analyze_response
"$output_file" "$loop_count" without "|| analysis_exit_code=$?" or replace it
with a conditional that logs or exits on failure (e.g., if ! analyze_response
"$output_file" "$loop_count"; then processLogger/error or exit with $?);
reference analyze_response, output_file, loop_count, and analysis_exit_code when
making the change.
tests/unit/test_cli_modern.bats (2)

532-535: Inline test function diverges from production implementation.

The inline build_claude_command uses --continue (line 534), but the production code in ralph_loop.sh (lines 1127-1140 per relevant snippets) uses --resume "$session_id" instead. The production code explicitly avoids --continue because it "can hijack active Claude Code sessions" (Issue #151).

While this may not break current tests (since CLAUDE_USE_CONTINUE is often false in test scenarios), it could mask regressions if session continuity behavior is tested. Consider syncing this inline copy with the production implementation.

♻️ Proposed sync with production
     # Add session continuity flag
-    if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
-        CLAUDE_CMD_ARGS+=("--continue")
+    # IMPORTANT: Use --resume with explicit session ID instead of --continue
+    # --continue resumes the "most recent session in current directory" which
+    # can hijack active Claude Code sessions. --resume with a specific session ID
+    # ensures we only resume Ralph's own sessions. (Issue `#151`)
+    if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
+        CLAUDE_CMD_ARGS+=("--resume" "$session_id")
     fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_cli_modern.bats` around lines 532 - 535, The test helper's
inline build_claude_command currently appends "--continue" when
CLAUDE_USE_CONTINUE is true, diverging from production ralph_loop.sh which uses
"--resume \"$session_id\"" to avoid hijacking sessions; update the test to
mirror production by replacing the "--continue" branch so it constructs
CLAUDE_CMD_ARGS with "--resume" and the appropriate session identifier (use the
same session variable name used in tests) when CLAUDE_USE_CONTINUE is true,
keeping the CLAUDE_CMD_ARGS array usage and any existing conditional logic
intact so tests exercise the same session-continuity behavior as ralph_loop.sh.

621-621: Update test assertion if inline function is synced.

If the inline build_claude_command is updated to use --resume instead of --continue (as in production), this assertion at line 621 would need to change accordingly.

♻️ Proposed update (if sync is applied)
-    [[ "$cmd_string" == *"--continue"* ]]
+    [[ "$cmd_string" == *"--resume"* ]]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_cli_modern.bats` at line 621, The test assertion in
tests/unit/test_cli_modern.bats currently checks for "--continue" in the
generated command string (variable cmd_string); if the inline function
build_claude_command has been synced to use "--resume" in production, update
this assertion to look for "--resume" instead of "--continue" (search for
references to cmd_string and the test assertion and replace the pattern match [[
"$cmd_string" == *"--continue"* ]] with the equivalent that checks for
"--resume"); also scan the file for any other assertions or expectations that
reference "--continue" and update them to "--resume" to keep tests consistent
with build_claude_command.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ralph_loop.sh`:
- Around line 1602-1625: The EXIT trap is causing cleanup() to run on normal
successful exits and overwrite the final status; change the signal handling to
only invoke cleanup on actual interruption signals (e.g., SIGINT and SIGTERM)
instead of EXIT. Replace or modify the existing trap line (trap cleanup EXIT) so
that cleanup is registered for SIGINT and SIGTERM (trap 'cleanup' SIGINT
SIGTERM) and ensure normal successful termination from main() is allowed to set
final status without being clobbered by cleanup's reset_session/update_status
logic that references loop_count, reset_session, and update_status.

In `@tests/unit/test_cli_modern.bats`:
- Around line 1453-1461: The grep command in the test "update_exit_signals and
log_analysis_summary use || true for set -e safety" uses single quotes around
'$script' which prevents expansion; update the run line so $script is expanded
inside the bash -c string by using escaped double quotes around the variable
(e.g. run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file
change count/p' \"$script\")"), ensuring the inner double quotes are escaped so
the variable $script is interpolated correctly.

---

Nitpick comments:
In `@ralph_loop.sh`:
- Around line 1490-1491: The variable analysis_exit_code is assigned but never
used after calling analyze_response("$output_file" "$loop_count"), so either
remove the unused capture or handle non-zero results; modify the call in
ralph_loop.sh to either just call analyze_response "$output_file" "$loop_count"
without "|| analysis_exit_code=$?" or replace it with a conditional that logs or
exits on failure (e.g., if ! analyze_response "$output_file" "$loop_count"; then
processLogger/error or exit with $?); reference analyze_response, output_file,
loop_count, and analysis_exit_code when making the change.

In `@tests/unit/test_cli_modern.bats`:
- Around line 532-535: The test helper's inline build_claude_command currently
appends "--continue" when CLAUDE_USE_CONTINUE is true, diverging from production
ralph_loop.sh which uses "--resume \"$session_id\"" to avoid hijacking sessions;
update the test to mirror production by replacing the "--continue" branch so it
constructs CLAUDE_CMD_ARGS with "--resume" and the appropriate session
identifier (use the same session variable name used in tests) when
CLAUDE_USE_CONTINUE is true, keeping the CLAUDE_CMD_ARGS array usage and any
existing conditional logic intact so tests exercise the same session-continuity
behavior as ralph_loop.sh.
- Line 621: The test assertion in tests/unit/test_cli_modern.bats currently
checks for "--continue" in the generated command string (variable cmd_string);
if the inline function build_claude_command has been synced to use "--resume" in
production, update this assertion to look for "--resume" instead of "--continue"
(search for references to cmd_string and the test assertion and replace the
pattern match [[ "$cmd_string" == *"--continue"* ]] with the equivalent that
checks for "--resume"); also scan the file for any other assertions or
expectations that reference "--continue" and update them to "--resume" to keep
tests consistent with build_claude_command.

In `@tests/unit/test_file_protection.bats`:
- Around line 187-194: The test "RALPH_REQUIRED_PATHS contains all critical
paths and excludes optional files" currently only asserts that "status.json" is
not required; update the test to also assert that ".ralph/logs/",
".ralph/.call_count", and ".ralph/.exit_signals" are not present in
RALPH_REQUIRED_PATHS. Locate the test block (function name or description) and
add negative membership checks for each optional path against the
RALPH_REQUIRED_PATHS array so all four optional files listed in the comment are
verified as excluded.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff27b4 and 9147fd9.

📒 Files selected for processing (15)
  • CLAUDE.md
  • lib/circuit_breaker.sh
  • lib/enable_core.sh
  • lib/response_analyzer.sh
  • ralph_loop.sh
  • setup.sh
  • templates/ralphrc.template
  • tests/unit/test_circuit_breaker_recovery.bats
  • tests/unit/test_cli_modern.bats
  • tests/unit/test_exit_detection.bats
  • tests/unit/test_file_protection.bats
  • tests/unit/test_integrity_check.bats
  • tests/unit/test_json_parsing.bats
  • tests/unit/test_rate_limiting.bats
  • tests/unit/test_session_continuity.bats
💤 Files with no reviewable changes (1)
  • tests/unit/test_integrity_check.bats

Comment on lines 1602 to 1625
cleanup() {
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
reset_session "manual_interrupt"
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
exit 0
# Reentrancy guard — prevent double execution from EXIT + signal combination
if [[ "$_CLEANUP_DONE" == "true" ]]; then return; fi
_CLEANUP_DONE=true

# Kill background Claude process if still running
if [[ -n "$_CLAUDE_PID" ]] && kill -0 "$_CLAUDE_PID" 2>/dev/null; then
kill "$_CLAUDE_PID" 2>/dev/null || true
wait "$_CLAUDE_PID" 2>/dev/null || true
fi
_CLAUDE_PID=""

# Only perform session/status cleanup if main loop was running
if [[ $loop_count -gt 0 ]]; then
log_status "INFO" "Ralph loop interrupted. Cleaning up..."
reset_session "manual_interrupt"
update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
fi
# No exit here — EXIT trap handles natural termination
}

# Set up signal handlers
trap cleanup SIGINT SIGTERM
# Set up signal handlers — EXIT covers SIGINT, SIGTERM, set -e errors, and normal exit
trap cleanup EXIT

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

EXIT trap currently clobbers successful completion state.

On Line 1624, trap cleanup EXIT runs cleanup() on normal exits too. Since Lines 1615-1619 only gate on loop_count > 0, successful runs are rewritten as manual_interrupt/interrupted, overwriting the final status set by main().

💡 Proposed fix
 cleanup() {
+    local exit_code=$?
     # Reentrancy guard — prevent double execution from EXIT + signal combination
     if [[ "$_CLEANUP_DONE" == "true" ]]; then return; fi
     _CLEANUP_DONE=true
@@
-    # Only perform session/status cleanup if main loop was running
-    if [[ $loop_count -gt 0 ]]; then
+    # Only perform interrupt-style cleanup on abnormal termination
+    if [[ $loop_count -gt 0 && $exit_code -ne 0 ]]; then
         log_status "INFO" "Ralph loop interrupted. Cleaning up..."
         reset_session "manual_interrupt"
         update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
     fi
     # No exit here — EXIT trap handles natural termination
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ralph_loop.sh` around lines 1602 - 1625, The EXIT trap is causing cleanup()
to run on normal successful exits and overwrite the final status; change the
signal handling to only invoke cleanup on actual interruption signals (e.g.,
SIGINT and SIGTERM) instead of EXIT. Replace or modify the existing trap line
(trap cleanup EXIT) so that cleanup is registered for SIGINT and SIGTERM (trap
'cleanup' SIGINT SIGTERM) and ensure normal successful termination from main()
is allowed to set final status without being clobbered by cleanup's
reset_session/update_status logic that references loop_count, reset_session, and
update_status.

Comment on lines +1453 to +1461
@test "update_exit_signals and log_analysis_summary use || true for set -e safety" {
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"

# Both non-critical analysis functions should have || true
run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file change count/p' '$script')"
assert_success
# Should find at least 2 (update_exit_signals || true and log_analysis_summary || true)
[[ "$output" -ge 2 ]]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Bug: Single quotes prevent variable expansion in grep command.

The $script variable inside single quotes at line 1457 won't be expanded. This will cause the test to fail or look for a literal $script file.

🐛 Proposed fix
-    run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file change count/p' '$script')"
+    run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file change count/p' \"$script\")"

Note: The escaped double quotes (\") are needed because this is inside a bash -c "..." string.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@test "update_exit_signals and log_analysis_summary use || true for set -e safety" {
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Both non-critical analysis functions should have || true
run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file change count/p' '$script')"
assert_success
# Should find at least 2 (update_exit_signals || true and log_analysis_summary || true)
[[ "$output" -ge 2 ]]
}
`@test` "update_exit_signals and log_analysis_summary use || true for set -e safety" {
local script="${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
# Both non-critical analysis functions should have || true
run bash -c "grep -c '|| true' <(sed -n '/Analyze the response/,/Get file change count/p' \"$script\")"
assert_success
# Should find at least 2 (update_exit_signals || true and log_analysis_summary || true)
[[ "$output" -ge 2 ]]
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_cli_modern.bats` around lines 1453 - 1461, The grep command
in the test "update_exit_signals and log_analysis_summary use || true for set -e
safety" uses single quotes around '$script' which prevents expansion; update the
run line so $script is expanded inside the bash -c string by using escaped
double quotes around the variable (e.g. run bash -c "grep -c '|| true' <(sed -n
'/Analyze the response/,/Get file change count/p' \"$script\")"), ensuring the
inner double quotes are escaped so the variable $script is interpolated
correctly.

…termination (frankbria#190)

Layer 1: Add || true to 22 non-fatal function calls in main() and CLI
parsing sections (reset_session, update_status, init_session_tracking,
init_call_tracking, update_session_last_used, show_circuit_status,
reset_circuit_breaker).

Layer 2: Fix log_circuit_transition data corruption bug — jq failure
on corrupted history file would overwrite it with empty string. Now
falls back to single-entry array preserving the current transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
ralph_loop.sh (1)

1603-1624: ⚠️ Potential issue | 🟠 Major

EXIT trap still overwrites successful completion as an interrupt.

Line 1615 only checks loop_count > 0, so normal successful exits still run interrupt cleanup and rewrite status/session state.

🔧 Proposed fix
 cleanup() {
+    local exit_code=$?
     # Reentrancy guard — prevent double execution from EXIT + signal combination
     if [[ "$_CLEANUP_DONE" == "true" ]]; then return; fi
     _CLEANUP_DONE=true
@@
-    # Only perform session/status cleanup if main loop was running
-    if [[ $loop_count -gt 0 ]]; then
+    # Only perform interrupt cleanup when exiting abnormally
+    if [[ $loop_count -gt 0 && $exit_code -ne 0 ]]; then
         log_status "INFO" "Ralph loop interrupted. Cleaning up..."
         reset_session "manual_interrupt"
         update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
     fi
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ralph_loop.sh` around lines 1603 - 1624, The EXIT trap's cleanup function is
treating normal successful completion as an interrupt because it only checks
loop_count; add a completion flag (e.g. _RALPH_COMPLETED or similar) that the
main loop sets right before a normal/successful exit, then update cleanup (the
cleanup function that the trap calls) to skip the interrupt-specific actions
(reset_session and update_status) when that completion flag is set; keep the
existing loop_count check but require both loop_count -gt 0 AND the completion
flag to be false before logging "Ralph loop interrupted" and calling
reset_session/update_status so trap cleanup only rewrites status on real
interrupts, not successful exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Line 180: The document contains inconsistent hard-coded test counts around the
"Run all tests" header and the test-results table; reconcile them by choosing a
single source of truth (either compute the total from the table rows or remove
the hard-coded total), then update the "Run all tests" header value and every
other occurrence of the total so they match the computed sum of the table rows
(or remove all totals so only individual row counts remain); make sure the table
rows themselves accurately sum to the chosen total and update any related
sentence that references the total.

In `@ralph_loop.sh`:
- Around line 1489-1496: The code currently ignores a failing analyze_response
call (analysis_exit_code is set but not acted upon), which can let
update_exit_signals and log_analysis_summary operate on stale .response_analysis
state; modify the control flow in the block that calls analyze_response,
update_exit_signals, and log_analysis_summary so that if analyze_response
returns non-zero you either (a) skip calling update_exit_signals and
log_analysis_summary and propagate or handle the error immediately, or (b)
refresh/clear the .response_analysis state before calling them; specifically
change the sequence around the analyze_response invocation in the function that
declares analysis_exit_code so that analyze_response's exit code is checked and
handled (using analyze_response, update_exit_signals, and log_analysis_summary
identifiers to locate the code).
- Around line 651-657: In check_claude_updates(), avoid hard-exit when npm is
missing by first checking for the npm binary (e.g., if ! command -v npm
>/dev/null; then log_status "INFO" "npm not found; skipping Claude CLI update
check" and return 0), and guard the command substitution so a failing npm
invocation doesn’t propagate under set -e (for example run the npm view call in
a subshell with its exit ignored or capture its exit/status and set
latest_version to empty on failure); update references to latest_version
assignment and any early returns in check_claude_updates() accordingly so
missing or failing npm simply logs and returns 0 rather than terminating the
script.

---

Duplicate comments:
In `@ralph_loop.sh`:
- Around line 1603-1624: The EXIT trap's cleanup function is treating normal
successful completion as an interrupt because it only checks loop_count; add a
completion flag (e.g. _RALPH_COMPLETED or similar) that the main loop sets right
before a normal/successful exit, then update cleanup (the cleanup function that
the trap calls) to skip the interrupt-specific actions (reset_session and
update_status) when that completion flag is set; keep the existing loop_count
check but require both loop_count -gt 0 AND the completion flag to be false
before logging "Ralph loop interrupted" and calling reset_session/update_status
so trap cleanup only rewrites status on real interrupts, not successful exits.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9147fd9 and 27fd652.

📒 Files selected for processing (5)
  • CLAUDE.md
  • lib/circuit_breaker.sh
  • ralph_loop.sh
  • tests/unit/test_circuit_breaker_recovery.bats
  • tests/unit/test_cli_modern.bats

### Running Tests
```bash
# Run all tests (568 tests)
# Run all tests (554 tests)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test-count values are inconsistent in this doc.

Line 180 says 554, Line 537 says 559, and the table rows currently sum to a different value. Please normalize this to a single source of truth (or remove hard-coded totals).

📝 Suggested docs-stability tweak
-# Run all tests (554 tests)
+# Run all tests
 npm test
@@
-### Test Files (559 tests total)
+### Test Files

Also applies to: 537-559

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 180, The document contains inconsistent hard-coded test
counts around the "Run all tests" header and the test-results table; reconcile
them by choosing a single source of truth (either compute the total from the
table rows or remove the hard-coded total), then update the "Run all tests"
header value and every other occurrence of the total so they match the computed
sum of the table rows (or remove all totals so only individual row counts
remain); make sure the table rows themselves accurately sum to the chosen total
and update any related sentence that references the total.

Comment on lines +651 to +657
# Query latest version from npm registry
local latest_version
latest_version=$(npm view @anthropic-ai/claude-code version 2>/dev/null)
if [[ -z "$latest_version" ]]; then
log_status "INFO" "Could not check for Claude CLI updates (npm registry unreachable)"
return 0
fi
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ralph_loop.sh
script=$(fd -a ralph_loop.sh | head -1)
echo "Script location: $script"

# Check if it exists
if [[ ! -f "$script" ]]; then
  echo "ERROR: ralph_loop.sh not found"
  exit 1
fi

# Extract the check_claude_updates function
echo -e "\n=== check_claude_updates() function ==="
sed -n '/^check_claude_updates()/,/^}/p' "$script" | head -60

Repository: frankbria/ralph-claude-code

Length of output: 1892


🏁 Script executed:

#!/bin/bash
# Check current state of lines 651-657 specifically
script=$(fd -a ralph_loop.sh | head -1)
echo "=== Lines 640-670 of ralph_loop.sh ==="
sed -n '640,670p' "$script" | nl -ba -v 640

Repository: frankbria/ralph-claude-code

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check if set -e is active when check_claude_updates is called
script=$(fd -a ralph_loop.sh | head -1)
echo "=== Checking set -e usage in script ==="
grep -n "set -e\|set +e" "$script" | head -20

Repository: frankbria/ralph-claude-code

Length of output: 499


🏁 Script executed:

#!/bin/bash
# Verify the actual impact of npm command failure under set -e
echo "=== Testing npm failure behavior under set -e ==="
bash -c 'set -e; x=$(npm view 2>/dev/null); echo "After npm call, x=[$x]"; echo "REACHED_HERE"'
echo "Exit code: $?"

Repository: frankbria/ralph-claude-code

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Check where check_claude_updates is called
script=$(fd -a ralph_loop.sh | head -1)
echo "=== Calls to check_claude_updates ==="
grep -n "check_claude_updates" "$script"

Repository: frankbria/ralph-claude-code

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Check if npm is documented as a requirement
echo "=== Checking README and CLAUDE.md for npm requirement ==="
for file in README.md CLAUDE.md readme.md; do
  if [[ -f "$file" ]]; then
    echo "=== $file ==="
    grep -i "npm\|nodejs\|node.js\|install\|require" "$file" | head -20
  fi
done

Repository: frankbria/ralph-claude-code

Length of output: 3112


🏁 Script executed:

#!/bin/bash
# Check what context check_claude_updates is called in
script=$(fd -a ralph_loop.sh | head -1)
echo "=== Context around check_claude_updates call ==="
grep -B5 -A5 "check_claude_updates" "$script" | grep -v "^check_claude_updates()"

Repository: frankbria/ralph-claude-code

Length of output: 776


Guard npm command availability in check_claude_updates() to prevent startup hard-exit.

At line 653, the command substitution latest_version=$(npm view ...) will terminate the entire script under set -e when npm is not available, regardless of stderr redirection. This breaks the intended non-blocking startup behavior — the function is designed to gracefully handle missing updates but currently fails if the npm binary itself doesn't exist.

The fix requires two changes:

  1. Check npm availability before attempting the command
  2. Guard the assignment with proper error handling to prevent subshell exit propagation
Proposed fix
check_claude_updates() {
    if [[ "${CLAUDE_AUTO_UPDATE:-true}" != "true" ]]; then
        return 0
    fi

+   if ! command -v npm >/dev/null 2>&1; then
+       log_status "INFO" "Skipping Claude CLI auto-update check (npm not available)"
+       return 0
+   fi

    local installed_version
     installed_version=$($CLAUDE_CODE_CMD --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
     if [[ -z "$installed_version" ]]; then
         return 0
     fi

     # Query latest version from npm registry
     local latest_version
-    latest_version=$(npm view `@anthropic-ai/claude-code` version 2>/dev/null)
-    if [[ -z "$latest_version" ]]; then
+    if ! latest_version=$(npm view `@anthropic-ai/claude-code` version 2>/dev/null) || [[ -z "$latest_version" ]]; then
         log_status "INFO" "Could not check for Claude CLI updates (npm registry unreachable)"
         return 0
     fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ralph_loop.sh` around lines 651 - 657, In check_claude_updates(), avoid
hard-exit when npm is missing by first checking for the npm binary (e.g., if !
command -v npm >/dev/null; then log_status "INFO" "npm not found; skipping
Claude CLI update check" and return 0), and guard the command substitution so a
failing npm invocation doesn’t propagate under set -e (for example run the npm
view call in a subshell with its exit ignored or capture its exit/status and set
latest_version to empty on failure); update references to latest_version
assignment and any early returns in check_claude_updates() accordingly so
missing or failing npm simply logs and returns 0 rather than terminating the
script.

Comment on lines +1489 to +1496
local analysis_exit_code=0
analyze_response "$output_file" "$loop_count" || analysis_exit_code=$?

# Update exit signals based on analysis
update_exit_signals
update_exit_signals || true

# Log analysis summary
log_analysis_summary
log_analysis_summary || true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle analyze_response failure before using derived analysis signals.

analysis_exit_code is captured but ignored. If analysis fails, downstream calls can consume stale .response_analysis state and skew exit/circuit decisions.

🔧 Proposed fix
         log_status "INFO" "🔍 Analyzing Claude Code response..."
         local analysis_exit_code=0
         analyze_response "$output_file" "$loop_count" || analysis_exit_code=$?
-
-        # Update exit signals based on analysis
-        update_exit_signals || true
-
-        # Log analysis summary
-        log_analysis_summary || true
+        if [[ $analysis_exit_code -eq 0 ]]; then
+            # Update exit signals based on analysis
+            update_exit_signals || true
+            # Log analysis summary
+            log_analysis_summary || true
+        else
+            log_status "WARN" "Response analysis failed (exit $analysis_exit_code); skipping analysis-derived signal updates"
+            rm -f "$RESPONSE_ANALYSIS_FILE"
+        fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 1490-1490: analysis_exit_code appears unused. Verify use (or export if used externally).

(SC2034)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ralph_loop.sh` around lines 1489 - 1496, The code currently ignores a failing
analyze_response call (analysis_exit_code is set but not acted upon), which can
let update_exit_signals and log_analysis_summary operate on stale
.response_analysis state; modify the control flow in the block that calls
analyze_response, update_exit_signals, and log_analysis_summary so that if
analyze_response returns non-zero you either (a) skip calling
update_exit_signals and log_analysis_summary and propagate or handle the error
immediately, or (b) refresh/clear the .response_analysis state before calling
them; specifically change the sequence around the analyze_response invocation in
the function that declares analysis_exit_code so that analyze_response's exit
code is checked and handled (using analyze_response, update_exit_signals, and
log_analysis_summary identifiers to locate the code).

@timothy-20 timothy-20 closed this Feb 28, 2026
@timothy-20 timothy-20 deleted the fix/set-e-bare-calls-190 branch February 28, 2026 07:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant