bmalph 2.7.5 → 2.7.7

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.
Files changed (67) hide show
  1. package/README.md +20 -5
  2. package/dist/commands/doctor-runtime-checks.js +104 -86
  3. package/dist/commands/doctor-runtime-checks.js.map +1 -1
  4. package/dist/commands/run.js +11 -2
  5. package/dist/commands/run.js.map +1 -1
  6. package/dist/commands/watch.js +5 -0
  7. package/dist/commands/watch.js.map +1 -1
  8. package/dist/installer/bmad-assets.js +182 -0
  9. package/dist/installer/bmad-assets.js.map +1 -0
  10. package/dist/installer/commands.js +324 -0
  11. package/dist/installer/commands.js.map +1 -0
  12. package/dist/installer/install.js +42 -0
  13. package/dist/installer/install.js.map +1 -0
  14. package/dist/installer/metadata.js +56 -0
  15. package/dist/installer/metadata.js.map +1 -0
  16. package/dist/installer/project-files.js +169 -0
  17. package/dist/installer/project-files.js.map +1 -0
  18. package/dist/installer/ralph-assets.js +91 -0
  19. package/dist/installer/ralph-assets.js.map +1 -0
  20. package/dist/installer/template-files.js +187 -0
  21. package/dist/installer/template-files.js.map +1 -0
  22. package/dist/installer/types.js +2 -0
  23. package/dist/installer/types.js.map +1 -0
  24. package/dist/installer.js +5 -843
  25. package/dist/installer.js.map +1 -1
  26. package/dist/run/run-dashboard.js +20 -6
  27. package/dist/run/run-dashboard.js.map +1 -1
  28. package/dist/transition/artifact-loading.js +91 -0
  29. package/dist/transition/artifact-loading.js.map +1 -0
  30. package/dist/transition/context-output.js +85 -0
  31. package/dist/transition/context-output.js.map +1 -0
  32. package/dist/transition/context.js +11 -3
  33. package/dist/transition/context.js.map +1 -1
  34. package/dist/transition/fix-plan-sync.js +119 -0
  35. package/dist/transition/fix-plan-sync.js.map +1 -0
  36. package/dist/transition/orchestration.js +25 -362
  37. package/dist/transition/orchestration.js.map +1 -1
  38. package/dist/transition/specs-sync.js +78 -2
  39. package/dist/transition/specs-sync.js.map +1 -1
  40. package/dist/utils/ralph-runtime-state.js +222 -0
  41. package/dist/utils/ralph-runtime-state.js.map +1 -0
  42. package/dist/utils/state.js +17 -16
  43. package/dist/utils/state.js.map +1 -1
  44. package/dist/utils/validate.js +16 -0
  45. package/dist/utils/validate.js.map +1 -1
  46. package/dist/watch/dashboard.js +25 -21
  47. package/dist/watch/dashboard.js.map +1 -1
  48. package/dist/watch/frame-writer.js +83 -0
  49. package/dist/watch/frame-writer.js.map +1 -0
  50. package/dist/watch/renderer.js +214 -49
  51. package/dist/watch/renderer.js.map +1 -1
  52. package/dist/watch/state-reader.js +87 -44
  53. package/dist/watch/state-reader.js.map +1 -1
  54. package/package.json +1 -1
  55. package/ralph/RALPH-REFERENCE.md +34 -14
  56. package/ralph/drivers/claude-code.sh +27 -0
  57. package/ralph/drivers/codex.sh +11 -0
  58. package/ralph/drivers/copilot.sh +11 -0
  59. package/ralph/drivers/cursor.sh +11 -0
  60. package/ralph/lib/circuit_breaker.sh +3 -3
  61. package/ralph/lib/date_utils.sh +28 -9
  62. package/ralph/lib/enable_core.sh +10 -2
  63. package/ralph/lib/response_analyzer.sh +252 -40
  64. package/ralph/ralph_import.sh +9 -1
  65. package/ralph/ralph_loop.sh +548 -128
  66. package/ralph/templates/PROMPT.md +20 -5
  67. package/ralph/templates/ralphrc.template +14 -4
@@ -22,6 +22,103 @@ RALPH_DIR="${RALPH_DIR:-.ralph}"
22
22
  COMPLETION_KEYWORDS=("done" "complete" "finished" "all tasks complete" "project complete" "ready for review")
23
23
  TEST_ONLY_PATTERNS=("npm test" "bats" "pytest" "jest" "cargo test" "go test" "running tests")
24
24
  NO_WORK_PATTERNS=("nothing to do" "no changes" "already implemented" "up to date")
25
+ PERMISSION_DENIAL_INLINE_PATTERNS=(
26
+ "requires approval before it can run"
27
+ "requires approval before it can proceed"
28
+ "not allowed to use tool"
29
+ "not permitted to use tool"
30
+ )
31
+
32
+ extract_permission_signal_text() {
33
+ local text=$1
34
+
35
+ if [[ -z "$text" ]]; then
36
+ echo ""
37
+ return 0
38
+ fi
39
+
40
+ # Only inspect the response preamble for tool refusals. Later paragraphs and
41
+ # copied logs often contain old permission errors that should not halt Ralph.
42
+ local signal_source="${text//$'\r'/}"
43
+ if [[ "$signal_source" == *"---RALPH_STATUS---"* ]]; then
44
+ signal_source="${signal_source%%---RALPH_STATUS---*}"
45
+ fi
46
+
47
+ local signal_text=""
48
+ local non_empty_lines=0
49
+ local trimmed=""
50
+ local line=""
51
+
52
+ while IFS= read -r line; do
53
+ trimmed="${line#"${line%%[![:space:]]*}"}"
54
+ trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
55
+
56
+ if [[ -z "$trimmed" ]]; then
57
+ if [[ $non_empty_lines -gt 0 ]]; then
58
+ break
59
+ fi
60
+ continue
61
+ fi
62
+
63
+ signal_text+="$trimmed"$'\n'
64
+ ((non_empty_lines += 1))
65
+ if [[ $non_empty_lines -ge 5 ]]; then
66
+ break
67
+ fi
68
+ done <<< "$signal_source"
69
+
70
+ printf '%s' "$signal_text"
71
+ }
72
+
73
+ permission_denial_line_matches() {
74
+ local normalized=$1
75
+
76
+ case "$normalized" in
77
+ permission\ denied:*|denied\ permission:*)
78
+ [[ "$normalized" == *approval* || "$normalized" == *tool* || "$normalized" == *command* || "$normalized" == *blocked* || "$normalized" == *"not allowed"* || "$normalized" == *"not permitted"* ]]
79
+ return
80
+ ;;
81
+ approval\ required:*)
82
+ [[ "$normalized" == *run* || "$normalized" == *proceed* || "$normalized" == *tool* || "$normalized" == *command* || "$normalized" == *blocked* ]]
83
+ return
84
+ ;;
85
+ esac
86
+
87
+ return 1
88
+ }
89
+
90
+ contains_permission_denial_signal() {
91
+ local signal_text=$1
92
+
93
+ if [[ -z "$signal_text" ]]; then
94
+ return 1
95
+ fi
96
+
97
+ local line
98
+ while IFS= read -r line; do
99
+ local trimmed="${line#"${line%%[![:space:]]*}"}"
100
+ local normalized="${trimmed,,}"
101
+
102
+ if permission_denial_line_matches "$normalized"; then
103
+ return 0
104
+ fi
105
+
106
+ local pattern
107
+ for pattern in "${PERMISSION_DENIAL_INLINE_PATTERNS[@]}"; do
108
+ if [[ "$normalized" == *"$pattern"* ]]; then
109
+ return 0
110
+ fi
111
+ done
112
+ done <<< "$signal_text"
113
+
114
+ return 1
115
+ }
116
+
117
+ contains_permission_denial_text() {
118
+ local signal_text
119
+ signal_text=$(extract_permission_signal_text "$1")
120
+ contains_permission_denial_signal "$signal_text"
121
+ }
25
122
 
26
123
  # =============================================================================
27
124
  # JSON OUTPUT FORMAT DETECTION AND PARSING
@@ -299,6 +396,68 @@ trim_shell_whitespace() {
299
396
  printf '%s' "$value"
300
397
  }
301
398
 
399
+ extract_ralph_status_block_json() {
400
+ local text=$1
401
+ local normalized="${text//$'\r'/}"
402
+
403
+ if [[ "$normalized" != *"---RALPH_STATUS---"* ]]; then
404
+ return 1
405
+ fi
406
+
407
+ local block="${normalized#*---RALPH_STATUS---}"
408
+ if [[ "$block" == "$normalized" ]]; then
409
+ return 1
410
+ fi
411
+
412
+ if [[ "$block" == *"---END_RALPH_STATUS---"* ]]; then
413
+ block="${block%%---END_RALPH_STATUS---*}"
414
+ fi
415
+
416
+ local status=""
417
+ local exit_signal="false"
418
+ local exit_signal_found="false"
419
+ local tasks_completed_this_loop=0
420
+ local line=""
421
+ local trimmed=""
422
+ local value=""
423
+
424
+ while IFS= read -r line; do
425
+ trimmed=$(trim_shell_whitespace "$line")
426
+
427
+ case "$trimmed" in
428
+ STATUS:*)
429
+ value=$(trim_shell_whitespace "${trimmed#STATUS:}")
430
+ [[ -n "$value" ]] && status="$value"
431
+ ;;
432
+ EXIT_SIGNAL:*)
433
+ value=$(trim_shell_whitespace "${trimmed#EXIT_SIGNAL:}")
434
+ if [[ "$value" == "true" || "$value" == "false" ]]; then
435
+ exit_signal="$value"
436
+ exit_signal_found="true"
437
+ fi
438
+ ;;
439
+ TASKS_COMPLETED_THIS_LOOP:*)
440
+ value=$(trim_shell_whitespace "${trimmed#TASKS_COMPLETED_THIS_LOOP:}")
441
+ if [[ "$value" =~ ^-?[0-9]+$ ]]; then
442
+ tasks_completed_this_loop=$value
443
+ fi
444
+ ;;
445
+ esac
446
+ done <<< "$block"
447
+
448
+ jq -n \
449
+ --arg status "$status" \
450
+ --argjson exit_signal_found "$exit_signal_found" \
451
+ --argjson exit_signal "$exit_signal" \
452
+ --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
453
+ '{
454
+ status: $status,
455
+ exit_signal_found: $exit_signal_found,
456
+ exit_signal: $exit_signal,
457
+ tasks_completed_this_loop: $tasks_completed_this_loop
458
+ }'
459
+ }
460
+
302
461
  # Parse JSON response and extract structured fields
303
462
  # Creates .ralph/.json_parse_result with normalized analysis data
304
463
  # Supports FIVE JSON formats:
@@ -369,35 +528,38 @@ parse_json_response() {
369
528
  # Track whether EXIT_SIGNAL was explicitly provided (vs inferred from STATUS)
370
529
  local exit_signal=$(jq -r -j '.exit_signal // false' "$output_file" 2>/dev/null)
371
530
  local explicit_exit_signal_found=$(jq -r -j 'has("exit_signal")' "$output_file" 2>/dev/null)
531
+ local tasks_completed_this_loop=$(jq -r -j '.tasks_completed_this_loop // 0' "$output_file" 2>/dev/null)
532
+ if [[ ! "$tasks_completed_this_loop" =~ ^-?[0-9]+$ ]]; then
533
+ tasks_completed_this_loop=0
534
+ fi
372
535
 
373
- # Bug #1 Fix: If exit_signal is still false, check for RALPH_STATUS block in .result field
374
- # Claude CLI JSON format embeds the RALPH_STATUS block within the .result text field
375
- if [[ "$exit_signal" == "false" && "$has_result_field" == "true" ]]; then
376
- local result_text=$(jq -r -j '.result // ""' "$output_file" 2>/dev/null)
377
- if [[ -n "$result_text" ]] && echo "$result_text" | grep -q -- "---RALPH_STATUS---"; then
378
- # Extract EXIT_SIGNAL value from RALPH_STATUS block within result text
379
- local embedded_exit_sig
380
- embedded_exit_sig=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2)")
381
- if [[ -n "$embedded_exit_sig" ]]; then
382
- # Explicit EXIT_SIGNAL found in RALPH_STATUS block
383
- explicit_exit_signal_found="true"
384
- if [[ "$embedded_exit_sig" == "true" ]]; then
385
- exit_signal="true"
386
- [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Extracted EXIT_SIGNAL=true from .result RALPH_STATUS block" >&2
387
- else
388
- exit_signal="false"
389
- [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Extracted EXIT_SIGNAL=false from .result RALPH_STATUS block (respecting explicit intent)" >&2
390
- fi
391
- fi
392
- # Also check STATUS field as fallback ONLY when EXIT_SIGNAL was not specified
393
- # This respects explicit EXIT_SIGNAL: false which means "task complete, continue working"
394
- local embedded_status
395
- embedded_status=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "STATUS:" | cut -d: -f2)")
396
- if [[ "$embedded_status" == "COMPLETE" && "$explicit_exit_signal_found" != "true" ]]; then
397
- # STATUS: COMPLETE without any EXIT_SIGNAL field implies completion
398
- exit_signal="true"
399
- [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Inferred EXIT_SIGNAL=true from .result STATUS=COMPLETE (no explicit EXIT_SIGNAL found)" >&2
400
- fi
536
+ local result_text=""
537
+ if [[ "$has_result_field" == "true" ]]; then
538
+ result_text=$(jq -r -j '.result // ""' "$output_file" 2>/dev/null)
539
+ fi
540
+
541
+ local ralph_status_json=""
542
+ if [[ -n "$result_text" ]] && ralph_status_json=$(extract_ralph_status_block_json "$result_text" 2>/dev/null); then
543
+ local embedded_exit_signal_found
544
+ embedded_exit_signal_found=$(printf '%s' "$ralph_status_json" | jq -r -j '.exit_signal_found' 2>/dev/null)
545
+ local embedded_exit_sig
546
+ embedded_exit_sig=$(printf '%s' "$ralph_status_json" | jq -r -j '.exit_signal' 2>/dev/null)
547
+ local embedded_status
548
+ embedded_status=$(printf '%s' "$ralph_status_json" | jq -r -j '.status' 2>/dev/null)
549
+ local embedded_tasks_completed
550
+ embedded_tasks_completed=$(printf '%s' "$ralph_status_json" | jq -r -j '.tasks_completed_this_loop' 2>/dev/null)
551
+
552
+ if [[ "$embedded_tasks_completed" =~ ^-?[0-9]+$ ]]; then
553
+ tasks_completed_this_loop=$embedded_tasks_completed
554
+ fi
555
+
556
+ if [[ "$embedded_exit_signal_found" == "true" ]]; then
557
+ explicit_exit_signal_found="true"
558
+ exit_signal="$embedded_exit_sig"
559
+ [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Extracted EXIT_SIGNAL=$embedded_exit_sig from .result RALPH_STATUS block" >&2
560
+ elif [[ "$embedded_status" == "COMPLETE" && "$explicit_exit_signal_found" != "true" ]]; then
561
+ exit_signal="true"
562
+ [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && echo "DEBUG: Inferred EXIT_SIGNAL=true from .result STATUS=COMPLETE (no explicit EXIT_SIGNAL found)" >&2
401
563
  fi
402
564
  fi
403
565
 
@@ -452,6 +614,14 @@ parse_json_response() {
452
614
  denied_commands_json=$(jq -r -j '[.permission_denials[] | if .tool_name == "Bash" then "Bash(\(.tool_input.command // "?" | split("\n")[0] | .[0:60]))" else .tool_name // "unknown" end]' "$output_file" 2>/dev/null || echo "[]")
453
615
  fi
454
616
 
617
+ # Heuristic permission-denial matching is limited to the refusal-shaped
618
+ # response preamble, not arbitrary prose or copied logs later in the body.
619
+ if [[ "$has_permission_denials" != "true" ]] && contains_permission_denial_text "$summary"; then
620
+ has_permission_denials="true"
621
+ permission_denial_count=1
622
+ denied_commands_json='["permission_denied"]'
623
+ fi
624
+
455
625
  # Apply completion heuristics to normalized summary text when explicit structured
456
626
  # completion markers are absent. This keeps JSONL analysis aligned with text mode.
457
627
  local summary_has_completion_keyword="false"
@@ -535,6 +705,7 @@ parse_json_response() {
535
705
  --argjson loop_number "$loop_number" \
536
706
  --arg session_id "$session_id" \
537
707
  --argjson confidence "$confidence" \
708
+ --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
538
709
  --argjson has_permission_denials "$has_permission_denials" \
539
710
  --argjson permission_denial_count "$permission_denial_count" \
540
711
  --argjson denied_commands "$denied_commands_json" \
@@ -550,6 +721,7 @@ parse_json_response() {
550
721
  loop_number: $loop_number,
551
722
  session_id: $session_id,
552
723
  confidence: $confidence,
724
+ tasks_completed_this_loop: $tasks_completed_this_loop,
553
725
  has_permission_denials: $has_permission_denials,
554
726
  permission_denial_count: $permission_denial_count,
555
727
  denied_commands: $denied_commands,
@@ -582,6 +754,7 @@ analyze_response() {
582
754
  local exit_signal=false
583
755
  local work_summary=""
584
756
  local files_modified=0
757
+ local tasks_completed_this_loop=0
585
758
 
586
759
  # Read output file
587
760
  if [[ ! -f "$output_file" ]]; then
@@ -607,6 +780,7 @@ analyze_response() {
607
780
  is_stuck=$(jq -r -j '.is_stuck' "$json_parse_result_file" 2>/dev/null || echo "false")
608
781
  work_summary=$(jq -r -j '.summary' "$json_parse_result_file" 2>/dev/null || echo "")
609
782
  files_modified=$(jq -r -j '.files_modified' "$json_parse_result_file" 2>/dev/null || echo "0")
783
+ tasks_completed_this_loop=$(jq -r -j '.tasks_completed_this_loop // 0' "$json_parse_result_file" 2>/dev/null || echo "0")
610
784
  local json_confidence=$(jq -r -j '.confidence' "$json_parse_result_file" 2>/dev/null || echo "0")
611
785
  local session_id=$(jq -r -j '.session_id' "$json_parse_result_file" 2>/dev/null || echo "")
612
786
 
@@ -628,6 +802,10 @@ analyze_response() {
628
802
  confidence_score=$((json_confidence + 50))
629
803
  fi
630
804
 
805
+ if [[ ! "$tasks_completed_this_loop" =~ ^-?[0-9]+$ ]]; then
806
+ tasks_completed_this_loop=0
807
+ fi
808
+
631
809
  # Check for file changes via git (supplements JSON data)
632
810
  # Fix #141: Detect both uncommitted changes AND committed changes
633
811
  if command -v git &>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
@@ -679,6 +857,7 @@ analyze_response() {
679
857
  --argjson files_modified "$files_modified" \
680
858
  --argjson confidence_score "$confidence_score" \
681
859
  --argjson exit_signal "$exit_signal" \
860
+ --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
682
861
  --arg work_summary "$work_summary" \
683
862
  --argjson output_length "$output_length" \
684
863
  --argjson has_permission_denials "$has_permission_denials" \
@@ -697,6 +876,9 @@ analyze_response() {
697
876
  files_modified: $files_modified,
698
877
  confidence_score: $confidence_score,
699
878
  exit_signal: $exit_signal,
879
+ tasks_completed_this_loop: $tasks_completed_this_loop,
880
+ fix_plan_completed_delta: 0,
881
+ has_progress_tracking_mismatch: false,
700
882
  work_summary: $work_summary,
701
883
  output_length: $output_length,
702
884
  has_permission_denials: $has_permission_denials,
@@ -718,13 +900,23 @@ analyze_response() {
718
900
  local explicit_exit_signal_found=false
719
901
 
720
902
  # 1. Check for explicit structured output (if Claude follows schema)
721
- if grep -q -- "---RALPH_STATUS---" "$output_file"; then
722
- # Parse structured output
723
- local status=$(trim_shell_whitespace "$(grep "STATUS:" "$output_file" | cut -d: -f2)")
724
- local exit_sig=$(trim_shell_whitespace "$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2)")
903
+ local ralph_status_json=""
904
+ if ralph_status_json=$(extract_ralph_status_block_json "$output_content" 2>/dev/null); then
905
+ local status
906
+ status=$(printf '%s' "$ralph_status_json" | jq -r -j '.status' 2>/dev/null)
907
+ local exit_sig_found
908
+ exit_sig_found=$(printf '%s' "$ralph_status_json" | jq -r -j '.exit_signal_found' 2>/dev/null)
909
+ local exit_sig
910
+ exit_sig=$(printf '%s' "$ralph_status_json" | jq -r -j '.exit_signal' 2>/dev/null)
911
+ local parsed_tasks_completed
912
+ parsed_tasks_completed=$(printf '%s' "$ralph_status_json" | jq -r -j '.tasks_completed_this_loop' 2>/dev/null)
913
+
914
+ if [[ "$parsed_tasks_completed" =~ ^-?[0-9]+$ ]]; then
915
+ tasks_completed_this_loop=$parsed_tasks_completed
916
+ fi
725
917
 
726
918
  # If EXIT_SIGNAL is explicitly provided, respect it
727
- if [[ -n "$exit_sig" ]]; then
919
+ if [[ "$exit_sig_found" == "true" ]]; then
728
920
  explicit_exit_signal_found=true
729
921
  if [[ "$exit_sig" == "true" ]]; then
730
922
  has_completion_signal=true
@@ -873,8 +1065,18 @@ analyze_response() {
873
1065
  fi
874
1066
  fi
875
1067
 
1068
+ local has_permission_denials=false
1069
+ local permission_denial_count=0
1070
+ local denied_commands_json='[]'
1071
+ local permission_signal_text=""
1072
+ permission_signal_text=$(extract_permission_signal_text "$output_content")
1073
+ if contains_permission_denial_text "$work_summary" || contains_permission_denial_signal "$permission_signal_text"; then
1074
+ has_permission_denials=true
1075
+ permission_denial_count=1
1076
+ denied_commands_json='["permission_denied"]'
1077
+ fi
1078
+
876
1079
  # Write analysis results to file (text parsing path) using jq for safe construction
877
- # Note: Permission denial fields default to false/0 since text output doesn't include this data
878
1080
  jq -n \
879
1081
  --argjson loop_number "$loop_number" \
880
1082
  --arg timestamp "$(get_iso_timestamp)" \
@@ -887,8 +1089,12 @@ analyze_response() {
887
1089
  --argjson files_modified "$files_modified" \
888
1090
  --argjson confidence_score "$confidence_score" \
889
1091
  --argjson exit_signal "$exit_signal" \
1092
+ --argjson tasks_completed_this_loop "$tasks_completed_this_loop" \
890
1093
  --arg work_summary "$work_summary" \
891
1094
  --argjson output_length "$output_length" \
1095
+ --argjson has_permission_denials "$has_permission_denials" \
1096
+ --argjson permission_denial_count "$permission_denial_count" \
1097
+ --argjson denied_commands "$denied_commands_json" \
892
1098
  '{
893
1099
  loop_number: $loop_number,
894
1100
  timestamp: $timestamp,
@@ -902,11 +1108,14 @@ analyze_response() {
902
1108
  files_modified: $files_modified,
903
1109
  confidence_score: $confidence_score,
904
1110
  exit_signal: $exit_signal,
1111
+ tasks_completed_this_loop: $tasks_completed_this_loop,
1112
+ fix_plan_completed_delta: 0,
1113
+ has_progress_tracking_mismatch: false,
905
1114
  work_summary: $work_summary,
906
1115
  output_length: $output_length,
907
- has_permission_denials: false,
908
- permission_denial_count: 0,
909
- denied_commands: []
1116
+ has_permission_denials: $has_permission_denials,
1117
+ permission_denial_count: $permission_denial_count,
1118
+ denied_commands: $denied_commands
910
1119
  }
911
1120
  }' > "$analysis_result_file"
912
1121
 
@@ -930,6 +1139,8 @@ update_exit_signals() {
930
1139
  local has_completion_signal=$(jq -r -j '.analysis.has_completion_signal' "$analysis_file")
931
1140
  local loop_number=$(jq -r -j '.loop_number' "$analysis_file")
932
1141
  local has_progress=$(jq -r -j '.analysis.has_progress' "$analysis_file")
1142
+ local has_permission_denials=$(jq -r -j '.analysis.has_permission_denials // false' "$analysis_file")
1143
+ local has_progress_tracking_mismatch=$(jq -r -j '.analysis.has_progress_tracking_mismatch // false' "$analysis_file")
933
1144
 
934
1145
  # Read current exit signals
935
1146
  local signals=$(cat "$exit_signals_file" 2>/dev/null || echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}')
@@ -944,8 +1155,9 @@ update_exit_signals() {
944
1155
  fi
945
1156
  fi
946
1157
 
947
- # Update done_signals array
948
- if [[ "$has_completion_signal" == "true" ]]; then
1158
+ # Permission denials are handled in the same loop, so they must not become
1159
+ # completion state that can halt the next loop.
1160
+ if [[ "$has_permission_denials" != "true" && "$has_progress_tracking_mismatch" != "true" && "$has_completion_signal" == "true" ]]; then
949
1161
  signals=$(echo "$signals" | jq ".done_signals += [$loop_number]")
950
1162
  fi
951
1163
 
@@ -954,7 +1166,7 @@ update_exit_signals() {
954
1166
  # due to deterministic scoring (+50 for JSON format, +20 for result field).
955
1167
  # This caused premature exits after 5 loops. Now we respect Claude's explicit intent.
956
1168
  local exit_signal=$(jq -r -j '.analysis.exit_signal // false' "$analysis_file")
957
- if [[ "$exit_signal" == "true" ]]; then
1169
+ if [[ "$has_permission_denials" != "true" && "$has_progress_tracking_mismatch" != "true" && "$exit_signal" == "true" ]]; then
958
1170
  signals=$(echo "$signals" | jq ".completion_indicators += [$loop_number]")
959
1171
  fi
960
1172
 
@@ -353,9 +353,17 @@ You are Ralph, an autonomous AI development agent working on a [PROJECT NAME] pr
353
353
  - Search the codebase before assuming something isn't implemented
354
354
  - Use subagents for expensive operations (file searching, analysis)
355
355
  - Write comprehensive tests with clear documentation
356
- - Update @fix_plan.md with your learnings
356
+ - Toggle completed story checkboxes in @fix_plan.md without rewriting story lines
357
357
  - Commit working changes with descriptive messages
358
358
 
359
+ ## Progress Tracking (CRITICAL)
360
+ - Ralph tracks progress by counting story checkboxes in @fix_plan.md
361
+ - When you complete a story, change `- [ ]` to `- [x]` on that exact story line
362
+ - Do NOT remove, rewrite, or reorder story lines in @fix_plan.md
363
+ - Update the checkbox before committing so the monitor updates immediately
364
+ - Set `TASKS_COMPLETED_THIS_LOOP` to the exact number of story checkboxes toggled this loop
365
+ - Only valid values: 0 or 1
366
+
359
367
  ## 🧪 Testing Guidelines (CRITICAL)
360
368
  - LIMIT testing to ~20% of your total effort per loop
361
369
  - PRIORITIZE: Implementation > Documentation > Tests