bmalph 2.7.5 → 2.7.6

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 (53) 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/installer/bmad-assets.js +182 -0
  5. package/dist/installer/bmad-assets.js.map +1 -0
  6. package/dist/installer/commands.js +324 -0
  7. package/dist/installer/commands.js.map +1 -0
  8. package/dist/installer/install.js +42 -0
  9. package/dist/installer/install.js.map +1 -0
  10. package/dist/installer/metadata.js +56 -0
  11. package/dist/installer/metadata.js.map +1 -0
  12. package/dist/installer/project-files.js +169 -0
  13. package/dist/installer/project-files.js.map +1 -0
  14. package/dist/installer/ralph-assets.js +91 -0
  15. package/dist/installer/ralph-assets.js.map +1 -0
  16. package/dist/installer/template-files.js +168 -0
  17. package/dist/installer/template-files.js.map +1 -0
  18. package/dist/installer/types.js +2 -0
  19. package/dist/installer/types.js.map +1 -0
  20. package/dist/installer.js +5 -843
  21. package/dist/installer.js.map +1 -1
  22. package/dist/transition/artifact-loading.js +91 -0
  23. package/dist/transition/artifact-loading.js.map +1 -0
  24. package/dist/transition/context-output.js +85 -0
  25. package/dist/transition/context-output.js.map +1 -0
  26. package/dist/transition/fix-plan-sync.js +119 -0
  27. package/dist/transition/fix-plan-sync.js.map +1 -0
  28. package/dist/transition/orchestration.js +25 -362
  29. package/dist/transition/orchestration.js.map +1 -1
  30. package/dist/transition/specs-sync.js +78 -2
  31. package/dist/transition/specs-sync.js.map +1 -1
  32. package/dist/utils/ralph-runtime-state.js +222 -0
  33. package/dist/utils/ralph-runtime-state.js.map +1 -0
  34. package/dist/utils/state.js +17 -16
  35. package/dist/utils/state.js.map +1 -1
  36. package/dist/utils/validate.js +16 -0
  37. package/dist/utils/validate.js.map +1 -1
  38. package/dist/watch/renderer.js +48 -6
  39. package/dist/watch/renderer.js.map +1 -1
  40. package/dist/watch/state-reader.js +79 -44
  41. package/dist/watch/state-reader.js.map +1 -1
  42. package/package.json +1 -1
  43. package/ralph/RALPH-REFERENCE.md +33 -13
  44. package/ralph/drivers/claude-code.sh +25 -0
  45. package/ralph/drivers/codex.sh +11 -0
  46. package/ralph/drivers/copilot.sh +11 -0
  47. package/ralph/drivers/cursor.sh +11 -0
  48. package/ralph/lib/circuit_breaker.sh +3 -3
  49. package/ralph/lib/date_utils.sh +28 -9
  50. package/ralph/lib/response_analyzer.sh +127 -7
  51. package/ralph/ralph_loop.sh +464 -121
  52. package/ralph/templates/PROMPT.md +5 -0
  53. 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
@@ -452,6 +549,14 @@ parse_json_response() {
452
549
  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
550
  fi
454
551
 
552
+ # Heuristic permission-denial matching is limited to the refusal-shaped
553
+ # response preamble, not arbitrary prose or copied logs later in the body.
554
+ if [[ "$has_permission_denials" != "true" ]] && contains_permission_denial_text "$summary"; then
555
+ has_permission_denials="true"
556
+ permission_denial_count=1
557
+ denied_commands_json='["permission_denied"]'
558
+ fi
559
+
455
560
  # Apply completion heuristics to normalized summary text when explicit structured
456
561
  # completion markers are absent. This keeps JSONL analysis aligned with text mode.
457
562
  local summary_has_completion_keyword="false"
@@ -873,8 +978,18 @@ analyze_response() {
873
978
  fi
874
979
  fi
875
980
 
981
+ local has_permission_denials=false
982
+ local permission_denial_count=0
983
+ local denied_commands_json='[]'
984
+ local permission_signal_text=""
985
+ permission_signal_text=$(extract_permission_signal_text "$output_content")
986
+ if contains_permission_denial_text "$work_summary" || contains_permission_denial_signal "$permission_signal_text"; then
987
+ has_permission_denials=true
988
+ permission_denial_count=1
989
+ denied_commands_json='["permission_denied"]'
990
+ fi
991
+
876
992
  # 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
993
  jq -n \
879
994
  --argjson loop_number "$loop_number" \
880
995
  --arg timestamp "$(get_iso_timestamp)" \
@@ -889,6 +1004,9 @@ analyze_response() {
889
1004
  --argjson exit_signal "$exit_signal" \
890
1005
  --arg work_summary "$work_summary" \
891
1006
  --argjson output_length "$output_length" \
1007
+ --argjson has_permission_denials "$has_permission_denials" \
1008
+ --argjson permission_denial_count "$permission_denial_count" \
1009
+ --argjson denied_commands "$denied_commands_json" \
892
1010
  '{
893
1011
  loop_number: $loop_number,
894
1012
  timestamp: $timestamp,
@@ -904,9 +1022,9 @@ analyze_response() {
904
1022
  exit_signal: $exit_signal,
905
1023
  work_summary: $work_summary,
906
1024
  output_length: $output_length,
907
- has_permission_denials: false,
908
- permission_denial_count: 0,
909
- denied_commands: []
1025
+ has_permission_denials: $has_permission_denials,
1026
+ permission_denial_count: $permission_denial_count,
1027
+ denied_commands: $denied_commands
910
1028
  }
911
1029
  }' > "$analysis_result_file"
912
1030
 
@@ -930,6 +1048,7 @@ update_exit_signals() {
930
1048
  local has_completion_signal=$(jq -r -j '.analysis.has_completion_signal' "$analysis_file")
931
1049
  local loop_number=$(jq -r -j '.loop_number' "$analysis_file")
932
1050
  local has_progress=$(jq -r -j '.analysis.has_progress' "$analysis_file")
1051
+ local has_permission_denials=$(jq -r -j '.analysis.has_permission_denials // false' "$analysis_file")
933
1052
 
934
1053
  # Read current exit signals
935
1054
  local signals=$(cat "$exit_signals_file" 2>/dev/null || echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}')
@@ -944,8 +1063,9 @@ update_exit_signals() {
944
1063
  fi
945
1064
  fi
946
1065
 
947
- # Update done_signals array
948
- if [[ "$has_completion_signal" == "true" ]]; then
1066
+ # Permission denials are handled in the same loop, so they must not become
1067
+ # completion state that can halt the next loop.
1068
+ if [[ "$has_permission_denials" != "true" && "$has_completion_signal" == "true" ]]; then
949
1069
  signals=$(echo "$signals" | jq ".done_signals += [$loop_number]")
950
1070
  fi
951
1071
 
@@ -954,7 +1074,7 @@ update_exit_signals() {
954
1074
  # due to deterministic scoring (+50 for JSON format, +20 for result field).
955
1075
  # This caused premature exits after 5 loops. Now we respect Claude's explicit intent.
956
1076
  local exit_signal=$(jq -r -j '.analysis.exit_signal // false' "$analysis_file")
957
- if [[ "$exit_signal" == "true" ]]; then
1077
+ if [[ "$has_permission_denials" != "true" && "$exit_signal" == "true" ]]; then
958
1078
  signals=$(echo "$signals" | jq ".completion_indicators += [$loop_number]")
959
1079
  fi
960
1080