bmalph 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,6 +76,7 @@ _env_QUALITY_GATE_TIMEOUT="${QUALITY_GATE_TIMEOUT:-}"
76
76
  _env_QUALITY_GATE_ON_COMPLETION_ONLY="${QUALITY_GATE_ON_COMPLETION_ONLY:-}"
77
77
  _env_REVIEW_ENABLED="${REVIEW_ENABLED:-}"
78
78
  _env_REVIEW_INTERVAL="${REVIEW_INTERVAL:-}"
79
+ _env_REVIEW_MODE="${REVIEW_MODE:-}"
79
80
 
80
81
  # Now set defaults (only if not already set by environment)
81
82
  MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
@@ -116,6 +117,11 @@ REVIEW_FINDINGS_FILE="$RALPH_DIR/.review_findings.json"
116
117
  REVIEW_PROMPT_FILE="$RALPH_DIR/REVIEW_PROMPT.md"
117
118
  REVIEW_LAST_SHA_FILE="$RALPH_DIR/.review_last_sha"
118
119
 
120
+ # REVIEW_MODE is derived in initialize_runtime_context() after .ralphrc is loaded.
121
+ # This ensures backwards compat: old .ralphrc files with only REVIEW_ENABLED=true
122
+ # still map to enhanced mode. Env vars always win via the snapshot/restore mechanism.
123
+ REVIEW_MODE="${REVIEW_MODE:-off}"
124
+
119
125
  # Valid tool patterns for --allowed-tools validation
120
126
  # Default: Claude Code tools. Platform driver overwrites via driver_valid_tools() in main().
121
127
  # Validation runs in main() after load_platform_driver so the correct patterns are in effect.
@@ -267,6 +273,7 @@ load_ralphrc() {
267
273
  [[ -n "$_env_QUALITY_GATE_ON_COMPLETION_ONLY" ]] && QUALITY_GATE_ON_COMPLETION_ONLY="$_env_QUALITY_GATE_ON_COMPLETION_ONLY"
268
274
  [[ -n "$_env_REVIEW_ENABLED" ]] && REVIEW_ENABLED="$_env_REVIEW_ENABLED"
269
275
  [[ -n "$_env_REVIEW_INTERVAL" ]] && REVIEW_INTERVAL="$_env_REVIEW_INTERVAL"
276
+ [[ -n "$_env_REVIEW_MODE" ]] && REVIEW_MODE="$_env_REVIEW_MODE"
270
277
 
271
278
  normalize_claude_permission_mode
272
279
  RALPHRC_FILE="$config_file"
@@ -317,6 +324,14 @@ initialize_runtime_context() {
317
324
  fi
318
325
  fi
319
326
 
327
+ # Derive REVIEW_MODE after .ralphrc load so backwards-compat works:
328
+ # old .ralphrc files with only REVIEW_ENABLED=true map to enhanced mode.
329
+ if [[ "$REVIEW_MODE" == "off" && "$REVIEW_ENABLED" == "true" ]]; then
330
+ REVIEW_MODE="enhanced"
331
+ fi
332
+ # Keep REVIEW_ENABLED in sync for any code that checks it
333
+ [[ "$REVIEW_MODE" != "off" ]] && REVIEW_ENABLED="true" || REVIEW_ENABLED="false"
334
+
320
335
  # Load platform driver after config so PLATFORM_DRIVER can be overridden.
321
336
  load_platform_driver
322
337
  RUNTIME_CONTEXT_LOADED=true
@@ -357,7 +372,7 @@ get_tmux_base_index() {
357
372
  # Setup tmux session with monitor
358
373
  setup_tmux_session() {
359
374
  local session_name="ralph-$(date +%s)"
360
- local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
375
+ local ralph_home="${RALPH_HOME:-$SCRIPT_DIR}"
361
376
  local project_dir="$(pwd)"
362
377
 
363
378
  initialize_runtime_context
@@ -516,6 +531,20 @@ log_status() {
516
531
  echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
517
532
  }
518
533
 
534
+ # Human-readable label for a process exit code
535
+ describe_exit_code() {
536
+ local code=$1
537
+ case "$code" in
538
+ 0) echo "completed" ;;
539
+ 1) echo "error" ;;
540
+ 124) echo "timed out" ;;
541
+ 130) echo "interrupted (SIGINT)" ;;
542
+ 137) echo "killed (OOM or SIGKILL)" ;;
543
+ 143) echo "terminated (SIGTERM)" ;;
544
+ *) echo "error (exit $code)" ;;
545
+ esac
546
+ }
547
+
519
548
  # Update status JSON for external monitoring
520
549
  update_status() {
521
550
  local loop_count=$1
@@ -523,6 +552,7 @@ update_status() {
523
552
  local last_action=$3
524
553
  local status=$4
525
554
  local exit_reason=${5:-""}
555
+ local driver_exit_code=${6:-""}
526
556
 
527
557
  jq -n \
528
558
  --arg timestamp "$(get_iso_timestamp)" \
@@ -533,6 +563,7 @@ update_status() {
533
563
  --arg status "$status" \
534
564
  --arg exit_reason "$exit_reason" \
535
565
  --arg next_reset "$(get_next_hour_time)" \
566
+ --arg driver_exit_code "$driver_exit_code" \
536
567
  '{
537
568
  timestamp: $timestamp,
538
569
  loop_count: $loop_count,
@@ -541,7 +572,8 @@ update_status() {
541
572
  last_action: $last_action,
542
573
  status: $status,
543
574
  exit_reason: $exit_reason,
544
- next_reset: $next_reset
575
+ next_reset: $next_reset,
576
+ driver_exit_code: (if $driver_exit_code != "" then ($driver_exit_code | tonumber) else null end)
545
577
  }' > "$STATUS_FILE"
546
578
 
547
579
  # Merge quality gate status if results exist
@@ -617,6 +649,44 @@ validate_claude_permission_mode() {
617
649
  esac
618
650
  }
619
651
 
652
+ validate_git_repo() {
653
+ if ! command -v git &>/dev/null; then
654
+ log_status "ERROR" "git is not installed or not on PATH."
655
+ echo ""
656
+ echo "Ralph requires git for progress detection."
657
+ echo ""
658
+ echo "Install git:"
659
+ echo " macOS: brew install git (or: xcode-select --install)"
660
+ echo " Ubuntu: sudo apt-get install git"
661
+ echo " Windows: https://git-scm.com/downloads"
662
+ echo ""
663
+ echo "After installing, run this command again."
664
+ return 1
665
+ fi
666
+
667
+ if ! git rev-parse --git-dir &>/dev/null 2>&1; then
668
+ log_status "ERROR" "No git repository found in $(pwd)."
669
+ echo ""
670
+ echo "Ralph requires a git repository for progress detection."
671
+ echo ""
672
+ echo "To fix this, run:"
673
+ echo " git init && git add -A && git commit -m 'initial commit'"
674
+ return 1
675
+ fi
676
+
677
+ if ! git rev-parse HEAD &>/dev/null 2>&1; then
678
+ log_status "ERROR" "Git repository has no commits."
679
+ echo ""
680
+ echo "Ralph requires at least one commit for progress detection."
681
+ echo ""
682
+ echo "To fix this, run:"
683
+ echo " git add -A && git commit -m 'initial commit'"
684
+ return 1
685
+ fi
686
+
687
+ return 0
688
+ }
689
+
620
690
  warn_if_allowed_tools_ignored() {
621
691
  if driver_supports_tool_allowlist; then
622
692
  return 0
@@ -811,6 +881,51 @@ count_fix_plan_checkboxes() {
811
881
  printf '%s %s %s\n' "$completed_items" "$uncompleted_items" "$total_items"
812
882
  }
813
883
 
884
+ # Extract the first unchecked task line from @fix_plan.md.
885
+ # Returns the raw checkbox line trimmed of leading whitespace, capped at 100 chars.
886
+ # Outputs empty string if no unchecked tasks exist or file is missing.
887
+ # Args: $1 = path to @fix_plan.md (optional, defaults to $RALPH_DIR/@fix_plan.md)
888
+ extract_next_fix_plan_task() {
889
+ local fix_plan_file="${1:-$RALPH_DIR/@fix_plan.md}"
890
+ [[ -f "$fix_plan_file" ]] || return 0
891
+ local line
892
+ line=$(grep -m 1 -E "^[[:space:]]*- \[ \]" "$fix_plan_file" 2>/dev/null || true)
893
+ # Trim leading whitespace
894
+ line="${line#"${line%%[![:space:]]*}"}"
895
+ # Trim trailing whitespace
896
+ line="${line%"${line##*[![:space:]]}"}"
897
+ printf '%s' "${line:0:100}"
898
+ }
899
+
900
+ # Collapse completed story detail lines in @fix_plan.md.
901
+ # For each [x]/[X] story line, strips subsequent indented blockquote lines ( > ...).
902
+ # Incomplete stories keep their detail lines intact.
903
+ # Args: $1 = path to @fix_plan.md (modifies in place via atomic write)
904
+ collapse_completed_stories() {
905
+ local fix_plan_file="${1:-$RALPH_DIR/@fix_plan.md}"
906
+ [[ -f "$fix_plan_file" ]] || return 0
907
+
908
+ local tmp_file="${fix_plan_file}.collapse_tmp"
909
+ local skipping=false
910
+
911
+ while IFS= read -r line || [[ -n "$line" ]]; do
912
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*Story[[:space:]]+[0-9] ]]; then
913
+ skipping=true
914
+ printf '%s\n' "$line"
915
+ continue
916
+ fi
917
+
918
+ if $skipping && [[ "$line" =~ ^[[:space:]]+\> ]]; then
919
+ continue
920
+ fi
921
+
922
+ skipping=false
923
+ printf '%s\n' "$line"
924
+ done < "$fix_plan_file" > "$tmp_file"
925
+
926
+ mv "$tmp_file" "$fix_plan_file"
927
+ }
928
+
814
929
  enforce_fix_plan_progress_tracking() {
815
930
  local analysis_file=$1
816
931
  local completed_before=$2
@@ -1226,11 +1341,17 @@ validate_allowed_tools() {
1226
1341
  # Provides loop-specific context via --append-system-prompt
1227
1342
  build_loop_context() {
1228
1343
  local loop_count=$1
1344
+ local session_id="${2:-}"
1229
1345
  local context=""
1230
1346
 
1231
1347
  # Add loop number
1232
1348
  context="Loop #${loop_count}. "
1233
1349
 
1350
+ # Signal session continuity when resuming a valid session
1351
+ if [[ -n "$session_id" ]]; then
1352
+ context+="Session continued — do NOT re-read spec files. Resume implementation. "
1353
+ fi
1354
+
1234
1355
  # Extract incomplete tasks from @fix_plan.md
1235
1356
  # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
1236
1357
  if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
@@ -1239,6 +1360,13 @@ build_loop_context() {
1239
1360
  local total_tasks=0
1240
1361
  read -r completed_tasks incomplete_tasks total_tasks < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1241
1362
  context+="Remaining tasks: ${incomplete_tasks}. "
1363
+
1364
+ # Inject the next unchecked task to give the AI a clear directive
1365
+ local next_task
1366
+ next_task=$(extract_next_fix_plan_task "$RALPH_DIR/@fix_plan.md")
1367
+ if [[ -n "$next_task" ]]; then
1368
+ context+="Next: ${next_task}. "
1369
+ fi
1242
1370
  fi
1243
1371
 
1244
1372
  # Add circuit breaker state
@@ -1278,31 +1406,119 @@ build_loop_context() {
1278
1406
  fi
1279
1407
  fi
1280
1408
 
1409
+ # Add git diff summary from previous loop (last segment — truncated first if over budget)
1410
+ if [[ -f "$RALPH_DIR/.loop_diff_summary" ]]; then
1411
+ local diff_summary
1412
+ diff_summary=$(head -c 150 "$RALPH_DIR/.loop_diff_summary" 2>/dev/null)
1413
+ if [[ -n "$diff_summary" ]]; then
1414
+ context+="${diff_summary}. "
1415
+ fi
1416
+ fi
1417
+
1281
1418
  # Limit total length to ~500 chars
1282
1419
  echo "${context:0:500}"
1283
1420
  }
1284
1421
 
1285
- # Check if a periodic code review should run this iteration
1422
+ # Capture a compact git diff summary after each loop iteration.
1423
+ # Writes to $RALPH_DIR/.loop_diff_summary for the next loop's build_loop_context().
1424
+ # Args: $1 = loop_start_sha (git HEAD at loop start)
1425
+ capture_loop_diff_summary() {
1426
+ local loop_start_sha="${1:-}"
1427
+ local summary_file="$RALPH_DIR/.loop_diff_summary"
1428
+
1429
+ # Clear previous summary
1430
+ rm -f "$summary_file"
1431
+
1432
+ # Require git and a valid repo
1433
+ if ! command -v git &>/dev/null || ! git rev-parse --git-dir &>/dev/null 2>&1; then
1434
+ return 0
1435
+ fi
1436
+
1437
+ local current_sha
1438
+ current_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
1439
+ local numstat_output=""
1440
+
1441
+ if [[ -n "$loop_start_sha" && -n "$current_sha" && "$loop_start_sha" != "$current_sha" ]]; then
1442
+ # Commits exist: union of committed + working tree changes, deduplicated by filename
1443
+ numstat_output=$(
1444
+ {
1445
+ git diff --numstat "$loop_start_sha" HEAD 2>/dev/null
1446
+ git diff --numstat HEAD 2>/dev/null
1447
+ git diff --numstat --cached 2>/dev/null
1448
+ } | awk -F'\t' '!seen[$3]++'
1449
+ )
1450
+ else
1451
+ # No commits: staged + unstaged only
1452
+ numstat_output=$(
1453
+ {
1454
+ git diff --numstat 2>/dev/null
1455
+ git diff --numstat --cached 2>/dev/null
1456
+ } | awk -F'\t' '!seen[$3]++'
1457
+ )
1458
+ fi
1459
+
1460
+ [[ -z "$numstat_output" ]] && return 0
1461
+
1462
+ # Format: Changed: file (+add/-del), file2 (+add/-del)
1463
+ # Skip binary files (numstat shows - - for binary)
1464
+ # Use tab separator — numstat output is tab-delimited (handles filenames with spaces)
1465
+ local formatted
1466
+ formatted=$(echo "$numstat_output" | awk -F'\t' '
1467
+ $1 != "-" {
1468
+ if (n++) printf ", "
1469
+ printf "%s (+%s/-%s)", $3, $1, $2
1470
+ }
1471
+ ')
1472
+
1473
+ [[ -z "$formatted" ]] && return 0
1474
+
1475
+ local result="Changed: ${formatted}"
1476
+ # Self-truncate to ~150 chars (144 content + "...")
1477
+ if [[ ${#result} -gt 147 ]]; then
1478
+ result="${result:0:144}..."
1479
+ fi
1480
+
1481
+ echo "$result" > "$summary_file"
1482
+ }
1483
+
1484
+ # Check if a code review should run this iteration
1286
1485
  # Returns 0 (true) when review is due, 1 (false) otherwise
1486
+ # Args: $1 = loop_count, $2 = fix_plan_completed_delta (optional, for ultimate mode)
1287
1487
  should_run_review() {
1288
- [[ "$REVIEW_ENABLED" != "true" ]] && return 1
1488
+ [[ "$REVIEW_MODE" == "off" ]] && return 1
1289
1489
  local loop_count=$1
1490
+ local fix_plan_delta=${2:-0}
1491
+
1290
1492
  # Never review on first loop (no implementation yet)
1291
1493
  (( loop_count < 1 )) && return 1
1292
- (( loop_count % REVIEW_INTERVAL != 0 )) && return 1
1494
+
1293
1495
  # Skip if circuit breaker is not CLOSED
1294
1496
  if [[ -f "$RALPH_DIR/.circuit_breaker_state" ]]; then
1295
1497
  local cb_state
1296
1498
  cb_state=$(jq -r '.state // "CLOSED"' "$RALPH_DIR/.circuit_breaker_state" 2>/dev/null)
1297
1499
  [[ "$cb_state" != "CLOSED" ]] && return 1
1298
1500
  fi
1501
+
1502
+ # Mode-specific trigger
1503
+ case "$REVIEW_MODE" in
1504
+ enhanced)
1505
+ (( loop_count % REVIEW_INTERVAL != 0 )) && return 1
1506
+ ;;
1507
+ ultimate)
1508
+ (( fix_plan_delta < 1 )) && return 1
1509
+ ;;
1510
+ *)
1511
+ # Unknown mode — treat as off
1512
+ return 1
1513
+ ;;
1514
+ esac
1515
+
1299
1516
  # Skip if no changes since last review (committed or uncommitted)
1300
1517
  if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
1301
1518
  local current_sha last_sha
1302
1519
  current_sha=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
1303
1520
  last_sha=""
1304
1521
  [[ -f "$REVIEW_LAST_SHA_FILE" ]] && last_sha=$(cat "$REVIEW_LAST_SHA_FILE" 2>/dev/null)
1305
- # Check for new commits OR uncommitted workspace changes
1306
1522
  local has_uncommitted
1307
1523
  has_uncommitted=$(git status --porcelain 2>/dev/null | head -1)
1308
1524
  if [[ "$current_sha" == "$last_sha" && -z "$has_uncommitted" ]]; then
@@ -1313,7 +1529,8 @@ should_run_review() {
1313
1529
  }
1314
1530
 
1315
1531
  # Build review findings context for injection into the next implementation loop
1316
- # Returns a compact string (max 500 chars) with unresolved findings
1532
+ # Returns a compact string (max 500-700 chars) with unresolved findings
1533
+ # HIGH/CRITICAL findings get a PRIORITY prefix and a higher char cap (700)
1317
1534
  build_review_context() {
1318
1535
  if [[ ! -f "$REVIEW_FINDINGS_FILE" ]]; then
1319
1536
  echo ""
@@ -1330,7 +1547,15 @@ build_review_context() {
1330
1547
  return
1331
1548
  fi
1332
1549
 
1333
- local context="REVIEW FINDINGS ($severity, $issues_found issues): $summary"
1550
+ # HIGH/CRITICAL findings: instruct the AI to fix them before picking a new story
1551
+ local context=""
1552
+ local max_len=500
1553
+ if [[ "$severity" == "HIGH" || "$severity" == "CRITICAL" ]]; then
1554
+ context="PRIORITY: Fix these code review findings BEFORE picking a new story. "
1555
+ max_len=700
1556
+ fi
1557
+ context+="REVIEW FINDINGS ($severity, $issues_found issues): $summary"
1558
+
1334
1559
  # Include top details if space allows
1335
1560
  local top_details
1336
1561
  top_details=$(jq -r '(.details[:2] // []) | map("- [\(.severity)] \(.file): \(.issue)") | join("; ")' "$REVIEW_FINDINGS_FILE" 2>/dev/null | head -c 150)
@@ -1338,7 +1563,7 @@ build_review_context() {
1338
1563
  context+=" Details: $top_details"
1339
1564
  fi
1340
1565
 
1341
- echo "${context:0:500}"
1566
+ echo "${context:0:$max_len}"
1342
1567
  }
1343
1568
 
1344
1569
  # Execute a periodic code review loop (read-only, no file modifications)
@@ -1916,12 +2141,16 @@ get_live_stream_filter() {
1916
2141
  execute_claude_code() {
1917
2142
  local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
1918
2143
  local output_file="$LOG_DIR/claude_output_${timestamp}.log"
2144
+ local stderr_file="$LOG_DIR/claude_stderr_${timestamp}.log"
1919
2145
  local loop_count=$1
1920
2146
  local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1921
2147
  calls_made=$((calls_made + 1))
1922
2148
  local fix_plan_completed_before=0
1923
2149
  read -r fix_plan_completed_before _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1924
2150
 
2151
+ # Clear previous diff summary to prevent stale context on early exit (#117)
2152
+ rm -f "$RALPH_DIR/.loop_diff_summary"
2153
+
1925
2154
  # Fix #141: Capture git HEAD SHA at loop start to detect commits as progress
1926
2155
  # Store in file for access by progress detection after Claude execution
1927
2156
  local loop_start_sha=""
@@ -1934,21 +2163,22 @@ execute_claude_code() {
1934
2163
  local timeout_seconds=$((CLAUDE_TIMEOUT_MINUTES * 60))
1935
2164
  log_status "INFO" "⏳ Starting $DRIVER_DISPLAY_NAME execution... (timeout: ${CLAUDE_TIMEOUT_MINUTES}m)"
1936
2165
 
2166
+ # Initialize or resume session (must happen before build_loop_context
2167
+ # so the session_id can gate the "session continued" signal)
2168
+ local session_id=""
2169
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" ]] && supports_driver_sessions; then
2170
+ session_id=$(init_claude_session)
2171
+ fi
2172
+
1937
2173
  # Build loop context for session continuity
1938
2174
  local loop_context=""
1939
2175
  if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
1940
- loop_context=$(build_loop_context "$loop_count")
2176
+ loop_context=$(build_loop_context "$loop_count" "$session_id")
1941
2177
  if [[ -n "$loop_context" && "$VERBOSE_PROGRESS" == "true" ]]; then
1942
2178
  log_status "INFO" "Loop context: $loop_context"
1943
2179
  fi
1944
2180
  fi
1945
2181
 
1946
- # Initialize or resume session
1947
- local session_id=""
1948
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]] && supports_driver_sessions; then
1949
- session_id=$(init_claude_session)
1950
- fi
1951
-
1952
2182
  # Live mode requires JSON output (stream-json) — override text format
1953
2183
  if [[ "$LIVE_OUTPUT" == "true" && "$CLAUDE_OUTPUT_FORMAT" == "text" ]]; then
1954
2184
  log_status "WARN" "Live mode requires JSON output format. Overriding text → json for this session."
@@ -2037,7 +2267,7 @@ execute_claude_code() {
2037
2267
  # read from stdin even in -p (print) mode, causing the process to hang
2038
2268
  set -o pipefail
2039
2269
  portable_timeout ${timeout_seconds}s stdbuf -oL "${LIVE_CMD_ARGS[@]}" \
2040
- < /dev/null 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
2270
+ < /dev/null 2>"$stderr_file" | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
2041
2271
 
2042
2272
  # Capture exit codes from pipeline
2043
2273
  local -a pipe_status=("${PIPESTATUS[@]}")
@@ -2089,7 +2319,7 @@ execute_claude_code() {
2089
2319
  # stdin must be redirected from /dev/null because newer Claude CLI versions
2090
2320
  # read from stdin even in -p (print) mode, causing SIGTTIN suspension
2091
2321
  # when the process is backgrounded
2092
- if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>&1 &
2322
+ if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>"$stderr_file" &
2093
2323
  then
2094
2324
  : # Continue to wait loop
2095
2325
  else
@@ -2104,7 +2334,7 @@ execute_claude_code() {
2104
2334
  # Note: Legacy mode doesn't use --allowedTools, so tool permissions
2105
2335
  # will be handled by Claude Code's default permission system
2106
2336
  if [[ "$use_modern_cli" == "false" ]]; then
2107
- if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
2337
+ if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>"$stderr_file" &
2108
2338
  then
2109
2339
  : # Continue to wait loop
2110
2340
  else
@@ -2163,6 +2393,9 @@ EOF
2163
2393
  exit_code=$?
2164
2394
  fi
2165
2395
 
2396
+ # Expose the raw driver exit code to the main loop for status reporting
2397
+ LAST_DRIVER_EXIT_CODE=$exit_code
2398
+
2166
2399
  if [ $exit_code -eq 0 ]; then
2167
2400
  # Only increment counter on successful execution
2168
2401
  echo "$calls_made" > "$CALL_COUNT_FILE"
@@ -2186,6 +2419,11 @@ EOF
2186
2419
  read -r fix_plan_completed_after _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
2187
2420
  enforce_fix_plan_progress_tracking "$RESPONSE_ANALYSIS_FILE" "$fix_plan_completed_before" "$fix_plan_completed_after"
2188
2421
 
2422
+ # Collapse completed story details so the agent doesn't re-read them
2423
+ if [[ $fix_plan_completed_after -gt $fix_plan_completed_before ]]; then
2424
+ collapse_completed_stories "$RALPH_DIR/@fix_plan.md"
2425
+ fi
2426
+
2189
2427
  # Run quality gates
2190
2428
  local exit_signal_for_gates
2191
2429
  exit_signal_for_gates=$(jq -r '.analysis.exit_signal // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
@@ -2248,6 +2486,9 @@ EOF
2248
2486
  fi
2249
2487
  fi
2250
2488
 
2489
+ # Capture diff summary for next loop's context (#117)
2490
+ capture_loop_diff_summary "$loop_start_sha"
2491
+
2251
2492
  local has_errors="false"
2252
2493
 
2253
2494
  # Two-stage error detection to avoid JSON field false positives
@@ -2298,24 +2539,50 @@ EOF
2298
2539
  log_status "ERROR" "🚫 Claude API 5-hour usage limit reached"
2299
2540
  return 2 # Special return code for API limit
2300
2541
  else
2301
- log_status "ERROR" "❌ $DRIVER_DISPLAY_NAME execution failed, check: $output_file"
2542
+ local exit_desc
2543
+ exit_desc=$(describe_exit_code "$exit_code")
2544
+ log_status "ERROR" "❌ $DRIVER_DISPLAY_NAME exited: $exit_desc (code $exit_code)"
2545
+ if [[ -f "$stderr_file" && -s "$stderr_file" ]]; then
2546
+ log_status "ERROR" " stderr (last 3 lines): $(tail -3 "$stderr_file")"
2547
+ log_status "ERROR" " full stderr log: $stderr_file"
2548
+ fi
2302
2549
  return 1
2303
2550
  fi
2304
2551
  fi
2305
2552
  }
2306
2553
 
2307
- # Cleanup function
2308
- cleanup() {
2309
- log_status "INFO" "Ralph loop interrupted. Cleaning up..."
2554
+ # Guard against double cleanup (EXIT fires after signal handler exits)
2555
+ _CLEANUP_DONE=false
2556
+
2557
+ # EXIT trap — catches set -e failures and other unexpected exits
2558
+ _on_exit() {
2559
+ local code=$?
2560
+ [[ "$_CLEANUP_DONE" == "true" ]] && return
2561
+ _CLEANUP_DONE=true
2562
+ if [[ "$code" -ne 0 ]]; then
2563
+ local desc
2564
+ desc=$(describe_exit_code "$code")
2565
+ log_status "ERROR" "Ralph loop exiting unexpectedly: $desc (code $code)"
2566
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "unexpected_exit" "stopped" "$desc" "$code"
2567
+ fi
2568
+ }
2569
+
2570
+ # Signal handler — preserves signal identity in exit code
2571
+ _on_signal() {
2572
+ local sig=$1
2573
+ log_status "INFO" "Ralph loop interrupted by $sig. Cleaning up..."
2310
2574
  reset_session "manual_interrupt"
2311
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
2312
- exit 0
2575
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped" "$sig"
2576
+ _CLEANUP_DONE=true
2577
+ [[ "$sig" == "SIGINT" ]] && exit 130
2578
+ exit 143
2313
2579
  }
2314
2580
 
2315
- # Set up signal handlers
2316
- trap cleanup SIGINT SIGTERM
2581
+ trap _on_exit EXIT
2582
+ trap '_on_signal SIGINT' SIGINT
2583
+ trap '_on_signal SIGTERM' SIGTERM
2317
2584
 
2318
- # Global variable for loop count (needed by cleanup function)
2585
+ # Global variable for loop count (needed by trap handlers)
2319
2586
  loop_count=0
2320
2587
 
2321
2588
  # Main loop
@@ -2417,6 +2684,11 @@ main() {
2417
2684
  exit 1
2418
2685
  fi
2419
2686
 
2687
+ # Check for git repository (required for progress detection)
2688
+ if ! validate_git_repo; then
2689
+ exit 1
2690
+ fi
2691
+
2420
2692
  # Initialize session tracking before entering the loop
2421
2693
  init_session_tracking
2422
2694
 
@@ -2499,8 +2771,18 @@ main() {
2499
2771
 
2500
2772
  update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
2501
2773
 
2502
- # Periodic code review check
2503
- if should_run_review "$loop_count"; then
2774
+ # Consume review findings after successful execution — the AI has received
2775
+ # the context via --append-system-prompt. Deleting here (not in
2776
+ # build_review_context) ensures findings survive transient loop failures.
2777
+ rm -f "$REVIEW_FINDINGS_FILE"
2778
+
2779
+ # Code review check
2780
+ local fix_plan_delta=0
2781
+ if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
2782
+ fix_plan_delta=$(jq -r '.analysis.fix_plan_completed_delta // 0' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "0")
2783
+ [[ ! "$fix_plan_delta" =~ ^-?[0-9]+$ ]] && fix_plan_delta=0
2784
+ fi
2785
+ if should_run_review "$loop_count" "$fix_plan_delta"; then
2504
2786
  run_review_loop "$loop_count"
2505
2787
  fi
2506
2788
 
@@ -2551,7 +2833,12 @@ main() {
2551
2833
  printf "\n"
2552
2834
  fi
2553
2835
  else
2554
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
2836
+ # Infrastructure failures (timeout, crash, OOM) intentionally bypass
2837
+ # record_loop_result to avoid counting as agent stagnation. The circuit
2838
+ # breaker only tracks progress during successful executions. (Issue #145)
2839
+ local exit_desc
2840
+ exit_desc=$(describe_exit_code "${LAST_DRIVER_EXIT_CODE:-1}")
2841
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error" "$exit_desc" "${LAST_DRIVER_EXIT_CODE:-}"
2555
2842
  log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
2556
2843
  sleep 30
2557
2844
  fi
@@ -4,21 +4,28 @@
4
4
  You are Ralph, an autonomous AI development agent working on a [YOUR PROJECT NAME] project.
5
5
 
6
6
  ## Current Objectives
7
- 1. Study .ralph/specs/* to learn about the project specifications
8
- 2. Review .ralph/@fix_plan.md for current priorities
9
- 3. Implement the highest priority item using best practices
7
+ 1. Review .ralph/@fix_plan.md for current priorities
8
+ 2. Search the codebase for related code — especially which existing files need changes to integrate your work
9
+ 3. Implement the task from the loop context (or the first unchecked item in @fix_plan.md on the first loop)
10
10
  4. Use parallel subagents for complex tasks (max 100 concurrent)
11
11
  5. Run tests after each implementation
12
- 6. Update documentation and the completed story checkbox in @fix_plan.md
12
+ 6. Update the completed story checkbox in @fix_plan.md and commit
13
+ 7. Read .ralph/specs/* ONLY if the task requires specific context you don't already have
13
14
 
14
15
  ## Key Principles
15
- - ONE task per loop - focus on the most important thing
16
+ - Write code within the first few minutes of each loop
17
+ - ONE task per loop — implement the task specified in the loop context
16
18
  - Search the codebase before assuming something isn't implemented
19
+ - Creating new files is often only half the task — wire them into the existing application
17
20
  - Use subagents for expensive operations (file searching, analysis)
18
- - Write comprehensive tests with clear documentation
19
21
  - Toggle completed story checkboxes in .ralph/@fix_plan.md without rewriting story lines
20
22
  - Commit working changes with descriptive messages
21
23
 
24
+ ## Session Continuity
25
+ - If you have context from a previous loop, do NOT re-read spec files
26
+ - Resume implementation where you left off
27
+ - Only consult specs when you encounter ambiguity in the current task
28
+
22
29
  ## Progress Tracking (CRITICAL)
23
30
  - Ralph tracks progress by counting story checkboxes in .ralph/@fix_plan.md
24
31
  - When you complete a story, change `- [ ]` to `- [x]` on that exact story line
@@ -306,7 +313,7 @@ RECOMMENDATION: Blocked on [specific dependency] - need [what's needed]
306
313
  - examples/: Example usage and test cases
307
314
 
308
315
  ## Current Task
309
- Follow .ralph/@fix_plan.md and choose the most important item to implement next.
310
- Use your judgment to prioritize what will have the biggest impact on project progress.
316
+ Implement the task specified in the loop context.
317
+ If no task is specified (first loop), pick the first unchecked item from .ralph/@fix_plan.md.
311
318
 
312
319
  Remember: Quality over speed. Build it right the first time. Know when you're done.
@@ -129,13 +129,18 @@ QUALITY_GATE_ON_COMPLETION_ONLY="${QUALITY_GATE_ON_COMPLETION_ONLY:-false}"
129
129
  # PERIODIC CODE REVIEW
130
130
  # =============================================================================
131
131
 
132
- # Enable periodic code review loops (set via 'bmalph run --review' or manually)
133
- # When enabled, Ralph runs a read-only review session every REVIEW_INTERVAL loops.
132
+ # Review mode: off, enhanced, or ultimate (set via 'bmalph run --review [mode]')
133
+ # - off: no code review (default)
134
+ # - enhanced: periodic review every REVIEW_INTERVAL loops (~10-14% more tokens)
135
+ # - ultimate: review after every completed story (~20-30% more tokens)
134
136
  # The review agent analyzes git diffs and outputs findings for the next implementation loop.
135
137
  # Currently supported on Claude Code only.
138
+ REVIEW_MODE="${REVIEW_MODE:-off}"
139
+
140
+ # (Legacy) Enables review — prefer REVIEW_MODE instead
136
141
  REVIEW_ENABLED="${REVIEW_ENABLED:-false}"
137
142
 
138
- # Number of implementation loops between review sessions (default: 5)
143
+ # Number of implementation loops between review sessions (enhanced mode only)
139
144
  REVIEW_INTERVAL="${REVIEW_INTERVAL:-5}"
140
145
 
141
146
  # =============================================================================