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.
- package/README.md +7 -7
- package/dist/cli.js +2 -1
- package/dist/commands/doctor-checks.js +28 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/run.js +34 -17
- package/dist/installer/template-files.js +29 -0
- package/dist/run/ralph-process.js +20 -3
- package/dist/run/run-dashboard.js +6 -5
- package/dist/transition/context-output.js +3 -5
- package/dist/transition/context.js +54 -55
- package/dist/transition/fix-plan-sync.js +3 -2
- package/dist/transition/fix-plan.js +20 -0
- package/dist/transition/section-patterns.js +1 -1
- package/dist/transition/specs-index.js +2 -2
- package/dist/utils/format-status.js +13 -0
- package/dist/watch/renderer.js +2 -1
- package/dist/watch/state-reader.js +2 -0
- package/package.json +1 -1
- package/ralph/drivers/DRIVER_INTERFACE.md +422 -0
- package/ralph/drivers/codex.sh +2 -2
- package/ralph/lib/response_analyzer.sh +114 -102
- package/ralph/ralph_loop.sh +318 -31
- package/ralph/templates/PROMPT.md +15 -8
- package/ralph/templates/ralphrc.template +8 -3
package/ralph/ralph_loop.sh
CHANGED
|
@@ -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:-$
|
|
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
|
-
#
|
|
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
|
-
[[ "$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2316
|
-
trap
|
|
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
|
|
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
|
-
#
|
|
2503
|
-
|
|
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
|
-
|
|
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.
|
|
8
|
-
2.
|
|
9
|
-
3. Implement the
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
#
|
|
133
|
-
#
|
|
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 (
|
|
143
|
+
# Number of implementation loops between review sessions (enhanced mode only)
|
|
139
144
|
REVIEW_INTERVAL="${REVIEW_INTERVAL:-5}"
|
|
140
145
|
|
|
141
146
|
# =============================================================================
|