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
@@ -40,9 +40,32 @@ _env_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
40
40
  _env_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
41
41
  _env_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
42
42
  _env_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
43
+ _env_has_CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE+x}"
44
+ _env_CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE:-}"
43
45
  _env_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
44
46
  _env_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
47
+ _env_ALLOWED_TOOLS="${ALLOWED_TOOLS:-}"
48
+ _env_SESSION_CONTINUITY="${SESSION_CONTINUITY:-}"
49
+ _env_SESSION_EXPIRY_HOURS="${SESSION_EXPIRY_HOURS:-}"
50
+ _env_PERMISSION_DENIAL_MODE="${PERMISSION_DENIAL_MODE:-}"
51
+ _env_RALPH_VERBOSE="${RALPH_VERBOSE:-}"
45
52
  _env_VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-}"
53
+
54
+ # CLI flags are parsed before main() runs, so capture explicit values separately.
55
+ _CLI_MAX_CALLS_PER_HOUR="${_CLI_MAX_CALLS_PER_HOUR:-}"
56
+ _CLI_CLAUDE_TIMEOUT_MINUTES="${_CLI_CLAUDE_TIMEOUT_MINUTES:-}"
57
+ _CLI_CLAUDE_OUTPUT_FORMAT="${_CLI_CLAUDE_OUTPUT_FORMAT:-}"
58
+ _CLI_ALLOWED_TOOLS="${_CLI_ALLOWED_TOOLS:-}"
59
+ _CLI_SESSION_CONTINUITY="${_CLI_SESSION_CONTINUITY:-}"
60
+ _CLI_SESSION_EXPIRY_HOURS="${_CLI_SESSION_EXPIRY_HOURS:-}"
61
+ _CLI_VERBOSE_PROGRESS="${_CLI_VERBOSE_PROGRESS:-}"
62
+ _cli_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
63
+ _cli_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
64
+ _cli_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
65
+ _cli_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
66
+ _cli_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
67
+ _cli_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
68
+ _cli_VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-}"
46
69
  _env_CB_COOLDOWN_MINUTES="${CB_COOLDOWN_MINUTES:-}"
47
70
  _env_CB_AUTO_RESET="${CB_AUTO_RESET:-}"
48
71
 
@@ -50,11 +73,15 @@ _env_CB_AUTO_RESET="${CB_AUTO_RESET:-}"
50
73
  MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
51
74
  VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-false}"
52
75
  CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-15}"
76
+ DEFAULT_CLAUDE_ALLOWED_TOOLS="Write,Read,Edit,MultiEdit,Glob,Grep,Task,TodoWrite,WebFetch,WebSearch,NotebookEdit,Bash"
77
+ DEFAULT_PERMISSION_DENIAL_MODE="continue"
53
78
 
54
79
  # Modern Claude CLI configuration (Phase 1.1)
55
80
  CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
56
- CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)}"
81
+ CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-$DEFAULT_CLAUDE_ALLOWED_TOOLS}"
82
+ CLAUDE_PERMISSION_MODE="${CLAUDE_PERMISSION_MODE:-auto}"
57
83
  CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-true}"
84
+ PERMISSION_DENIAL_MODE="${PERMISSION_DENIAL_MODE:-$DEFAULT_PERMISSION_DENIAL_MODE}"
58
85
  CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
59
86
  CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
60
87
 
@@ -80,6 +107,7 @@ VALID_TOOL_PATTERNS=(
80
107
  "TodoWrite"
81
108
  "WebFetch"
82
109
  "WebSearch"
110
+ "AskUserQuestion"
83
111
  "Bash"
84
112
  "Bash(git *)"
85
113
  "Bash(npm *)"
@@ -88,6 +116,8 @@ VALID_TOOL_PATTERNS=(
88
116
  "Bash(node *)"
89
117
  "NotebookEdit"
90
118
  )
119
+ ALLOWED_TOOLS_IGNORED_WARNED=false
120
+ PERMISSION_DENIAL_ACTION=""
91
121
 
92
122
  # Exit detection configuration
93
123
  EXIT_SIGNALS_FILE="$RALPH_DIR/.exit_signals"
@@ -131,7 +161,9 @@ resolve_ralphrc_file() {
131
161
  # - MAX_CALLS_PER_HOUR
132
162
  # - CLAUDE_TIMEOUT_MINUTES
133
163
  # - CLAUDE_OUTPUT_FORMAT
134
- # - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS)
164
+ # - CLAUDE_PERMISSION_MODE
165
+ # - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS for Claude Code only)
166
+ # - PERMISSION_DENIAL_MODE
135
167
  # - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
136
168
  # - SESSION_EXPIRY_HOURS (mapped to CLAUDE_SESSION_EXPIRY_HOURS)
137
169
  # - CB_NO_PROGRESS_THRESHOLD
@@ -155,6 +187,9 @@ load_ralphrc() {
155
187
  if [[ -n "${ALLOWED_TOOLS:-}" ]]; then
156
188
  CLAUDE_ALLOWED_TOOLS="$ALLOWED_TOOLS"
157
189
  fi
190
+ if [[ -n "${PERMISSION_DENIAL_MODE:-}" ]]; then
191
+ PERMISSION_DENIAL_MODE="$PERMISSION_DENIAL_MODE"
192
+ fi
158
193
  if [[ -n "${SESSION_CONTINUITY:-}" ]]; then
159
194
  CLAUDE_USE_CONTINUE="$SESSION_CONTINUITY"
160
195
  fi
@@ -167,22 +202,57 @@ load_ralphrc() {
167
202
 
168
203
  # Restore ONLY values that were explicitly set via environment variables
169
204
  # (not script defaults). The _env_* variables were captured BEFORE defaults were set.
170
- # If _env_* is non-empty, the user explicitly set it in their environment.
205
+ # Internal CLAUDE_* variables are kept for backward compatibility.
171
206
  [[ -n "$_env_MAX_CALLS_PER_HOUR" ]] && MAX_CALLS_PER_HOUR="$_env_MAX_CALLS_PER_HOUR"
172
207
  [[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
173
208
  [[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
174
209
  [[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
210
+ if [[ "$_env_has_CLAUDE_PERMISSION_MODE" == "x" ]]; then
211
+ CLAUDE_PERMISSION_MODE="$_env_CLAUDE_PERMISSION_MODE"
212
+ fi
175
213
  [[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
176
214
  [[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
215
+ [[ -n "$_env_PERMISSION_DENIAL_MODE" ]] && PERMISSION_DENIAL_MODE="$_env_PERMISSION_DENIAL_MODE"
177
216
  [[ -n "$_env_VERBOSE_PROGRESS" ]] && VERBOSE_PROGRESS="$_env_VERBOSE_PROGRESS"
217
+
218
+ # Public aliases are the preferred external interface and win over the
219
+ # legacy internal environment variables when both are explicitly set.
220
+ [[ -n "$_env_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_ALLOWED_TOOLS"
221
+ [[ -n "$_env_SESSION_CONTINUITY" ]] && CLAUDE_USE_CONTINUE="$_env_SESSION_CONTINUITY"
222
+ [[ -n "$_env_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_SESSION_EXPIRY_HOURS"
223
+ [[ -n "$_env_RALPH_VERBOSE" ]] && VERBOSE_PROGRESS="$_env_RALPH_VERBOSE"
224
+
225
+ # CLI flags are the highest-priority runtime inputs because they are
226
+ # parsed before main() and would otherwise be overwritten by .ralphrc.
227
+ # Keep every config-backed CLI flag here so the precedence contract stays
228
+ # consistent: CLI > public env aliases > internal env vars > config.
229
+ [[ "$_CLI_MAX_CALLS_PER_HOUR" == "true" ]] && MAX_CALLS_PER_HOUR="$_cli_MAX_CALLS_PER_HOUR"
230
+ [[ "$_CLI_CLAUDE_TIMEOUT_MINUTES" == "true" ]] && CLAUDE_TIMEOUT_MINUTES="$_cli_CLAUDE_TIMEOUT_MINUTES"
231
+ [[ "$_CLI_CLAUDE_OUTPUT_FORMAT" == "true" ]] && CLAUDE_OUTPUT_FORMAT="$_cli_CLAUDE_OUTPUT_FORMAT"
232
+ [[ "$_CLI_ALLOWED_TOOLS" == "true" ]] && CLAUDE_ALLOWED_TOOLS="$_cli_CLAUDE_ALLOWED_TOOLS"
233
+ [[ "$_CLI_SESSION_CONTINUITY" == "true" ]] && CLAUDE_USE_CONTINUE="$_cli_CLAUDE_USE_CONTINUE"
234
+ [[ "$_CLI_SESSION_EXPIRY_HOURS" == "true" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_cli_CLAUDE_SESSION_EXPIRY_HOURS"
235
+ [[ "$_CLI_VERBOSE_PROGRESS" == "true" ]] && VERBOSE_PROGRESS="$_cli_VERBOSE_PROGRESS"
178
236
  [[ -n "$_env_CB_COOLDOWN_MINUTES" ]] && CB_COOLDOWN_MINUTES="$_env_CB_COOLDOWN_MINUTES"
179
237
  [[ -n "$_env_CB_AUTO_RESET" ]] && CB_AUTO_RESET="$_env_CB_AUTO_RESET"
180
238
 
239
+ normalize_claude_permission_mode
181
240
  RALPHRC_FILE="$config_file"
182
241
  RALPHRC_LOADED=true
183
242
  return 0
184
243
  }
185
244
 
245
+ driver_supports_tool_allowlist() {
246
+ return 1
247
+ }
248
+
249
+ driver_permission_denial_help() {
250
+ echo " - Review the active driver's permission or approval settings."
251
+ echo " - ALLOWED_TOOLS in $RALPHRC_FILE only applies to the Claude Code driver."
252
+ echo " - Keep CLAUDE_PERMISSION_MODE=auto for unattended Claude Code loops."
253
+ echo " - After updating permissions, reset the session and restart the loop."
254
+ }
255
+
186
256
  # Source platform driver
187
257
  load_platform_driver() {
188
258
  local driver_file="$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh"
@@ -323,8 +393,8 @@ setup_tmux_session() {
323
393
  if [[ "$CLAUDE_TIMEOUT_MINUTES" != "15" ]]; then
324
394
  ralph_cmd="$ralph_cmd --timeout $CLAUDE_TIMEOUT_MINUTES"
325
395
  fi
326
- # Forward --allowed-tools if non-default
327
- if [[ "$CLAUDE_ALLOWED_TOOLS" != "Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)" ]]; then
396
+ # Forward --allowed-tools only for drivers that support tool allowlists
397
+ if driver_supports_tool_allowlist && [[ "$CLAUDE_ALLOWED_TOOLS" != "$DEFAULT_CLAUDE_ALLOWED_TOOLS" ]]; then
328
398
  ralph_cmd="$ralph_cmd --allowed-tools '$CLAUDE_ALLOWED_TOOLS'"
329
399
  fi
330
400
  # Forward --no-continue if session continuity disabled
@@ -443,6 +513,175 @@ update_status() {
443
513
  }' > "$STATUS_FILE"
444
514
  }
445
515
 
516
+ validate_permission_denial_mode() {
517
+ local mode=$1
518
+
519
+ case "$mode" in
520
+ continue|halt|threshold)
521
+ return 0
522
+ ;;
523
+ *)
524
+ echo "Error: Invalid PERMISSION_DENIAL_MODE: '$mode'"
525
+ echo "Valid modes: continue halt threshold"
526
+ return 1
527
+ ;;
528
+ esac
529
+ }
530
+
531
+ normalize_claude_permission_mode() {
532
+ if [[ -z "${CLAUDE_PERMISSION_MODE:-}" ]]; then
533
+ CLAUDE_PERMISSION_MODE="auto"
534
+ fi
535
+ }
536
+
537
+ validate_claude_permission_mode() {
538
+ local mode=$1
539
+
540
+ case "$mode" in
541
+ auto|acceptEdits|bypassPermissions|default|dontAsk|plan)
542
+ return 0
543
+ ;;
544
+ *)
545
+ echo "Error: Invalid CLAUDE_PERMISSION_MODE: '$mode'"
546
+ echo "Valid modes: auto acceptEdits bypassPermissions default dontAsk plan"
547
+ return 1
548
+ ;;
549
+ esac
550
+ }
551
+
552
+ warn_if_allowed_tools_ignored() {
553
+ if driver_supports_tool_allowlist; then
554
+ return 0
555
+ fi
556
+
557
+ if [[ "$ALLOWED_TOOLS_IGNORED_WARNED" == "true" ]]; then
558
+ return 0
559
+ fi
560
+
561
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" || "$CLAUDE_ALLOWED_TOOLS" != "$DEFAULT_CLAUDE_ALLOWED_TOOLS" ]]; then
562
+ log_status "WARN" "ALLOWED_TOOLS/--allowed-tools is ignored by $DRIVER_DISPLAY_NAME."
563
+ ALLOWED_TOOLS_IGNORED_WARNED=true
564
+ fi
565
+
566
+ return 0
567
+ }
568
+
569
+ show_current_allowed_tools() {
570
+ if ! driver_supports_tool_allowlist; then
571
+ return 0
572
+ fi
573
+
574
+ if [[ -f "$RALPHRC_FILE" ]]; then
575
+ local current_tools=$(grep "^ALLOWED_TOOLS=" "$RALPHRC_FILE" 2>/dev/null | cut -d= -f2- | tr -d '"')
576
+ if [[ -n "$current_tools" ]]; then
577
+ echo -e "${BLUE}Current ALLOWED_TOOLS:${NC} $current_tools"
578
+ echo ""
579
+ fi
580
+ fi
581
+
582
+ return 0
583
+ }
584
+
585
+ response_analysis_has_permission_denials() {
586
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
587
+ return 1
588
+ fi
589
+
590
+ local has_permission_denials
591
+ has_permission_denials=$(jq -r '.analysis.has_permission_denials // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
592
+
593
+ [[ "$has_permission_denials" == "true" ]]
594
+ }
595
+
596
+ get_response_analysis_denied_commands() {
597
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
598
+ echo "unknown"
599
+ return 0
600
+ fi
601
+
602
+ jq -r '.analysis.denied_commands | join(", ")' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "unknown"
603
+ }
604
+
605
+ clear_response_analysis_permission_denials() {
606
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
607
+ return 0
608
+ fi
609
+
610
+ local tmp_file="$RESPONSE_ANALYSIS_FILE.tmp"
611
+ if jq '
612
+ (.analysis //= {}) |
613
+ .analysis.has_completion_signal = false |
614
+ .analysis.exit_signal = false |
615
+ .analysis.has_permission_denials = false |
616
+ .analysis.permission_denial_count = 0 |
617
+ .analysis.denied_commands = []
618
+ ' "$RESPONSE_ANALYSIS_FILE" > "$tmp_file" 2>/dev/null; then
619
+ mv "$tmp_file" "$RESPONSE_ANALYSIS_FILE"
620
+ return 0
621
+ fi
622
+
623
+ rm -f "$tmp_file" 2>/dev/null
624
+ return 1
625
+ }
626
+
627
+ handle_permission_denial() {
628
+ local loop_count=$1
629
+ local denied_cmds=${2:-unknown}
630
+ local calls_made
631
+ calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
632
+ PERMISSION_DENIAL_ACTION=""
633
+
634
+ case "$PERMISSION_DENIAL_MODE" in
635
+ continue|threshold)
636
+ log_status "WARN" "🚫 Permission denied in loop #$loop_count: $denied_cmds"
637
+ log_status "WARN" "PERMISSION_DENIAL_MODE=$PERMISSION_DENIAL_MODE - continuing execution"
638
+ update_status "$loop_count" "$calls_made" "permission_denied" "running"
639
+ PERMISSION_DENIAL_ACTION="continue"
640
+ return 0
641
+ ;;
642
+ halt)
643
+ log_status "ERROR" "🚫 Permission denied - halting loop"
644
+ reset_session "permission_denied"
645
+ update_status "$loop_count" "$calls_made" "permission_denied" "halted" "permission_denied"
646
+
647
+ echo ""
648
+ echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
649
+ echo -e "${RED}║ PERMISSION DENIED - Loop Halted ║${NC}"
650
+ echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
651
+ echo ""
652
+ echo -e "${YELLOW}$DRIVER_DISPLAY_NAME was denied permission to execute commands.${NC}"
653
+ echo ""
654
+ echo -e "${YELLOW}To fix this:${NC}"
655
+ driver_permission_denial_help
656
+ echo ""
657
+ show_current_allowed_tools
658
+ PERMISSION_DENIAL_ACTION="halt"
659
+ return 0
660
+ ;;
661
+ esac
662
+
663
+ return 1
664
+ }
665
+
666
+ consume_current_loop_permission_denial() {
667
+ local loop_count=$1
668
+ PERMISSION_DENIAL_ACTION=""
669
+
670
+ if ! response_analysis_has_permission_denials; then
671
+ return 1
672
+ fi
673
+
674
+ local denied_cmds
675
+ denied_cmds=$(get_response_analysis_denied_commands)
676
+
677
+ if ! clear_response_analysis_permission_denials; then
678
+ log_status "WARN" "Failed to clear permission denial markers from response analysis"
679
+ fi
680
+
681
+ handle_permission_denial "$loop_count" "$denied_cmds"
682
+ return 0
683
+ }
684
+
446
685
  # Check if we can make another call
447
686
  can_make_call() {
448
687
  local calls_made=0
@@ -508,21 +747,6 @@ should_exit_gracefully() {
508
747
 
509
748
  # Check for exit conditions
510
749
 
511
- # 0. Permission denials (highest priority - Issue #101)
512
- # When Claude Code is denied permission to run commands, halt immediately
513
- # to allow user to update .ralphrc ALLOWED_TOOLS configuration
514
- if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
515
- local has_permission_denials=$(jq -r '.analysis.has_permission_denials // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
516
- if [[ "$has_permission_denials" == "true" ]]; then
517
- local denied_count=$(jq -r '.analysis.permission_denial_count // 0' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "0")
518
- local denied_cmds=$(jq -r '.analysis.denied_commands | join(", ")' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "unknown")
519
- log_status "WARN" "🚫 Permission denied for $denied_count command(s): $denied_cmds"
520
- log_status "WARN" "Update ALLOWED_TOOLS in .ralphrc to include the required tools"
521
- echo "permission_denied"
522
- return 0
523
- fi
524
- fi
525
-
526
750
  # 1. Too many consecutive test-only loops
527
751
  if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
528
752
  log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
@@ -810,6 +1034,7 @@ save_claude_session() {
810
1034
  session_id=$(extract_session_id_from_output "$output_file" 2>/dev/null || echo "")
811
1035
  if [[ -n "$session_id" && "$session_id" != "null" ]]; then
812
1036
  echo "$session_id" > "$CLAUDE_SESSION_FILE"
1037
+ sync_ralph_session_with_driver "$session_id"
813
1038
  log_status "INFO" "Saved session: ${session_id:0:20}..."
814
1039
  fi
815
1040
  fi
@@ -819,6 +1044,101 @@ save_claude_session() {
819
1044
  # SESSION LIFECYCLE MANAGEMENT FUNCTIONS (Phase 1.2)
820
1045
  # =============================================================================
821
1046
 
1047
+ write_active_ralph_session() {
1048
+ local session_id=$1
1049
+ local created_at=$2
1050
+ local last_used=${3:-$created_at}
1051
+
1052
+ jq -n \
1053
+ --arg session_id "$session_id" \
1054
+ --arg created_at "$created_at" \
1055
+ --arg last_used "$last_used" \
1056
+ '{
1057
+ session_id: $session_id,
1058
+ created_at: $created_at,
1059
+ last_used: $last_used
1060
+ }' > "$RALPH_SESSION_FILE"
1061
+ }
1062
+
1063
+ write_inactive_ralph_session() {
1064
+ local reset_at=$1
1065
+ local reset_reason=$2
1066
+
1067
+ jq -n \
1068
+ --arg session_id "" \
1069
+ --arg reset_at "$reset_at" \
1070
+ --arg reset_reason "$reset_reason" \
1071
+ '{
1072
+ session_id: $session_id,
1073
+ reset_at: $reset_at,
1074
+ reset_reason: $reset_reason
1075
+ }' > "$RALPH_SESSION_FILE"
1076
+ }
1077
+
1078
+ get_ralph_session_state() {
1079
+ if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
1080
+ echo "missing"
1081
+ return 0
1082
+ fi
1083
+
1084
+ if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
1085
+ echo "invalid"
1086
+ return 0
1087
+ fi
1088
+
1089
+ local session_id_type
1090
+ session_id_type=$(
1091
+ jq -r 'if has("session_id") then (.session_id | type) else "missing" end' \
1092
+ "$RALPH_SESSION_FILE" 2>/dev/null
1093
+ ) || {
1094
+ echo "invalid"
1095
+ return 0
1096
+ }
1097
+
1098
+ if [[ "$session_id_type" != "string" ]]; then
1099
+ echo "invalid"
1100
+ return 0
1101
+ fi
1102
+
1103
+ local session_id
1104
+ session_id=$(jq -r '.session_id' "$RALPH_SESSION_FILE" 2>/dev/null) || {
1105
+ echo "invalid"
1106
+ return 0
1107
+ }
1108
+
1109
+ if [[ "$session_id" == "" ]]; then
1110
+ echo "inactive"
1111
+ return 0
1112
+ fi
1113
+
1114
+ local created_at_type
1115
+ created_at_type=$(
1116
+ jq -r 'if has("created_at") then (.created_at | type) else "missing" end' \
1117
+ "$RALPH_SESSION_FILE" 2>/dev/null
1118
+ ) || {
1119
+ echo "invalid"
1120
+ return 0
1121
+ }
1122
+
1123
+ if [[ "$created_at_type" != "string" ]]; then
1124
+ echo "invalid"
1125
+ return 0
1126
+ fi
1127
+
1128
+ local created_at
1129
+ created_at=$(jq -r '.created_at' "$RALPH_SESSION_FILE" 2>/dev/null) || {
1130
+ echo "invalid"
1131
+ return 0
1132
+ }
1133
+
1134
+ if ! is_usable_ralph_session_created_at "$created_at"; then
1135
+ echo "invalid"
1136
+ return 0
1137
+ fi
1138
+
1139
+ echo "active"
1140
+ }
1141
+
822
1142
  # Get current session ID from Ralph session file
823
1143
  # Returns: session ID string or empty if not found
824
1144
  get_session_id() {
@@ -840,6 +1160,65 @@ get_session_id() {
840
1160
  return 0
841
1161
  }
842
1162
 
1163
+ is_usable_ralph_session_created_at() {
1164
+ local created_at=$1
1165
+ if [[ -z "$created_at" || "$created_at" == "null" ]]; then
1166
+ return 1
1167
+ fi
1168
+
1169
+ local created_at_epoch
1170
+ created_at_epoch=$(parse_iso_to_epoch_strict "$created_at") || return 1
1171
+
1172
+ local now_epoch
1173
+ now_epoch=$(get_epoch_seconds)
1174
+
1175
+ [[ "$created_at_epoch" -le "$now_epoch" ]]
1176
+ }
1177
+
1178
+ get_active_session_created_at() {
1179
+ if [[ "$(get_ralph_session_state)" != "active" ]]; then
1180
+ echo ""
1181
+ return 0
1182
+ fi
1183
+
1184
+ local created_at
1185
+ created_at=$(jq -r '.created_at // ""' "$RALPH_SESSION_FILE" 2>/dev/null)
1186
+ if [[ "$created_at" == "null" ]]; then
1187
+ created_at=""
1188
+ fi
1189
+
1190
+ if ! is_usable_ralph_session_created_at "$created_at"; then
1191
+ echo ""
1192
+ return 0
1193
+ fi
1194
+
1195
+ echo "$created_at"
1196
+ }
1197
+
1198
+ sync_ralph_session_with_driver() {
1199
+ local driver_session_id=$1
1200
+ if [[ -z "$driver_session_id" || "$driver_session_id" == "null" ]]; then
1201
+ return 0
1202
+ fi
1203
+
1204
+ local ts
1205
+ ts=$(get_iso_timestamp)
1206
+
1207
+ if [[ "$(get_ralph_session_state)" == "active" ]]; then
1208
+ local current_session_id
1209
+ current_session_id=$(get_session_id)
1210
+ local current_created_at
1211
+ current_created_at=$(get_active_session_created_at)
1212
+
1213
+ if [[ "$current_session_id" == "$driver_session_id" && -n "$current_created_at" ]]; then
1214
+ write_active_ralph_session "$driver_session_id" "$current_created_at" "$ts"
1215
+ return 0
1216
+ fi
1217
+ fi
1218
+
1219
+ write_active_ralph_session "$driver_session_id" "$ts" "$ts"
1220
+ }
1221
+
843
1222
  # Reset session with reason logging
844
1223
  # Usage: reset_session "reason_for_reset"
845
1224
  reset_session() {
@@ -849,20 +1228,7 @@ reset_session() {
849
1228
  local reset_timestamp
850
1229
  reset_timestamp=$(get_iso_timestamp)
851
1230
 
852
- # Always create/overwrite the session file using jq for safe JSON escaping
853
- jq -n \
854
- --arg session_id "" \
855
- --arg created_at "" \
856
- --arg last_used "" \
857
- --arg reset_at "$reset_timestamp" \
858
- --arg reset_reason "$reason" \
859
- '{
860
- session_id: $session_id,
861
- created_at: $created_at,
862
- last_used: $last_used,
863
- reset_at: $reset_at,
864
- reset_reason: $reset_reason
865
- }' > "$RALPH_SESSION_FILE"
1231
+ write_inactive_ralph_session "$reset_timestamp" "$reason"
866
1232
 
867
1233
  # Also clear the Claude session file for consistency
868
1234
  rm -f "$CLAUDE_SESSION_FILE" 2>/dev/null
@@ -951,67 +1317,39 @@ init_session_tracking() {
951
1317
  local ts
952
1318
  ts=$(get_iso_timestamp)
953
1319
 
954
- # Create session file if it doesn't exist
955
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
956
- local new_session_id
957
- new_session_id=$(generate_session_id)
958
-
959
- jq -n \
960
- --arg session_id "$new_session_id" \
961
- --arg created_at "$ts" \
962
- --arg last_used "$ts" \
963
- --arg reset_at "" \
964
- --arg reset_reason "" \
965
- '{
966
- session_id: $session_id,
967
- created_at: $created_at,
968
- last_used: $last_used,
969
- reset_at: $reset_at,
970
- reset_reason: $reset_reason
971
- }' > "$RALPH_SESSION_FILE"
972
-
973
- log_status "INFO" "Initialized session tracking (session: $new_session_id)"
1320
+ local session_state
1321
+ session_state=$(get_ralph_session_state)
1322
+ if [[ "$session_state" == "active" ]]; then
974
1323
  return 0
975
1324
  fi
976
1325
 
977
- # Validate existing session file
978
- if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
1326
+ if [[ "$session_state" == "invalid" ]]; then
979
1327
  log_status "WARN" "Corrupted session file detected, recreating..."
980
- local new_session_id
981
- new_session_id=$(generate_session_id)
982
-
983
- jq -n \
984
- --arg session_id "$new_session_id" \
985
- --arg created_at "$ts" \
986
- --arg last_used "$ts" \
987
- --arg reset_at "$ts" \
988
- --arg reset_reason "corrupted_file_recovery" \
989
- '{
990
- session_id: $session_id,
991
- created_at: $created_at,
992
- last_used: $last_used,
993
- reset_at: $reset_at,
994
- reset_reason: $reset_reason
995
- }' > "$RALPH_SESSION_FILE"
996
1328
  fi
1329
+
1330
+ local new_session_id
1331
+ new_session_id=$(generate_session_id)
1332
+ write_active_ralph_session "$new_session_id" "$ts" "$ts"
1333
+
1334
+ log_status "INFO" "Initialized session tracking (session: $new_session_id)"
997
1335
  }
998
1336
 
999
1337
  # Update last_used timestamp in session file (called on each loop iteration)
1000
1338
  update_session_last_used() {
1001
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
1339
+ if [[ "$(get_ralph_session_state)" != "active" ]]; then
1002
1340
  return 0
1003
1341
  fi
1004
1342
 
1005
1343
  local ts
1006
1344
  ts=$(get_iso_timestamp)
1007
1345
 
1008
- # Update last_used in existing session file
1009
- local updated
1010
- updated=$(jq --arg last_used "$ts" '.last_used = $last_used' "$RALPH_SESSION_FILE" 2>/dev/null)
1011
- local jq_status=$?
1346
+ local session_id
1347
+ session_id=$(get_session_id)
1348
+ local created_at
1349
+ created_at=$(get_active_session_created_at)
1012
1350
 
1013
- if [[ $jq_status -eq 0 && -n "$updated" ]]; then
1014
- echo "$updated" > "$RALPH_SESSION_FILE"
1351
+ if [[ -n "$session_id" && -n "$created_at" ]]; then
1352
+ write_active_ralph_session "$session_id" "$created_at" "$ts"
1015
1353
  fi
1016
1354
  }
1017
1355
 
@@ -1435,11 +1773,31 @@ loop_count=0
1435
1773
  main() {
1436
1774
  initialize_runtime_context
1437
1775
 
1438
- # Validate --allowed-tools now that platform-specific VALID_TOOL_PATTERNS are loaded
1439
- if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! validate_allowed_tools "$CLAUDE_ALLOWED_TOOLS"; then
1776
+ if ! validate_permission_denial_mode "$PERMISSION_DENIAL_MODE"; then
1440
1777
  exit 1
1441
1778
  fi
1442
1779
 
1780
+ if [[ "$(driver_name)" == "claude-code" ]]; then
1781
+ normalize_claude_permission_mode
1782
+
1783
+ if ! validate_claude_permission_mode "$CLAUDE_PERMISSION_MODE"; then
1784
+ exit 1
1785
+ fi
1786
+ fi
1787
+
1788
+ if driver_supports_tool_allowlist; then
1789
+ # Validate --allowed-tools now that platform-specific VALID_TOOL_PATTERNS are loaded
1790
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! validate_allowed_tools "$CLAUDE_ALLOWED_TOOLS"; then
1791
+ exit 1
1792
+ fi
1793
+ else
1794
+ warn_if_allowed_tools_ignored
1795
+ fi
1796
+
1797
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! driver_supports_tool_allowlist; then
1798
+ _CLI_ALLOWED_TOOLS=""
1799
+ fi
1800
+
1443
1801
  log_status "SUCCESS" "🚀 Ralph loop starting with $DRIVER_DISPLAY_NAME"
1444
1802
  log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
1445
1803
  log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
@@ -1531,45 +1889,6 @@ main() {
1531
1889
  # Check for graceful exit conditions
1532
1890
  local exit_reason=$(should_exit_gracefully)
1533
1891
  if [[ "$exit_reason" != "" ]]; then
1534
- # Handle permission_denied specially (Issue #101)
1535
- if [[ "$exit_reason" == "permission_denied" ]]; then
1536
- log_status "ERROR" "🚫 Permission denied - halting loop"
1537
- reset_session "permission_denied"
1538
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "permission_denied" "halted" "permission_denied"
1539
-
1540
- # Display helpful guidance for resolving permission issues
1541
- echo ""
1542
- echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
1543
- echo -e "${RED}║ PERMISSION DENIED - Loop Halted ║${NC}"
1544
- echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
1545
- echo ""
1546
- echo -e "${YELLOW}$DRIVER_DISPLAY_NAME was denied permission to execute commands.${NC}"
1547
- echo ""
1548
- echo -e "${YELLOW}To fix this:${NC}"
1549
- echo " 1. Edit .ralphrc and update ALLOWED_TOOLS to include the required tools"
1550
- echo " 2. Common patterns:"
1551
- echo " - Bash(npm *) - All npm commands"
1552
- echo " - Bash(npm install) - Only npm install"
1553
- echo " - Bash(pnpm *) - All pnpm commands"
1554
- echo " - Bash(yarn *) - All yarn commands"
1555
- echo ""
1556
- echo -e "${YELLOW}After updating .ralphrc:${NC}"
1557
- echo " bash .ralph/ralph_loop.sh --reset-session # Clear stale session state"
1558
- echo " bmalph run # Restart the loop"
1559
- echo ""
1560
-
1561
- # Show current ALLOWED_TOOLS if .ralphrc exists
1562
- if [[ -f ".ralphrc" ]]; then
1563
- local current_tools=$(grep "^ALLOWED_TOOLS=" ".ralphrc" 2>/dev/null | cut -d= -f2- | tr -d '"')
1564
- if [[ -n "$current_tools" ]]; then
1565
- echo -e "${BLUE}Current ALLOWED_TOOLS:${NC} $current_tools"
1566
- echo ""
1567
- fi
1568
- fi
1569
-
1570
- break
1571
- fi
1572
-
1573
1892
  log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
1574
1893
  reset_session "project_complete"
1575
1894
  update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
@@ -1591,6 +1910,17 @@ main() {
1591
1910
  local exec_result=$?
1592
1911
 
1593
1912
  if [ $exec_result -eq 0 ]; then
1913
+ if consume_current_loop_permission_denial "$loop_count"; then
1914
+ if [[ "$PERMISSION_DENIAL_ACTION" == "halt" ]]; then
1915
+ break
1916
+ fi
1917
+
1918
+ # Brief pause between loops when the denial was recorded but
1919
+ # policy allows Ralph to continue.
1920
+ sleep 5
1921
+ continue
1922
+ fi
1923
+
1594
1924
  update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
1595
1925
 
1596
1926
  # Brief pause between successful executions
@@ -1676,7 +2006,7 @@ Options:
1676
2006
  Modern CLI Options (Phase 1.1):
1677
2007
  --output-format FORMAT Set driver output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
1678
2008
  Note: --live mode requires JSON and will auto-switch
1679
- --allowed-tools TOOLS Comma-separated list of allowed tools (default: $CLAUDE_ALLOWED_TOOLS)
2009
+ --allowed-tools TOOLS Claude Code only. Ignored by codex, cursor, and copilot
1680
2010
  --no-continue Disable session continuity across loops
1681
2011
  --session-expiry HOURS Set session expiration time in hours (default: $CLAUDE_SESSION_EXPIRY_HOURS)
1682
2012
 
@@ -1722,6 +2052,8 @@ while [[ $# -gt 0 ]]; do
1722
2052
  ;;
1723
2053
  -c|--calls)
1724
2054
  MAX_CALLS_PER_HOUR="$2"
2055
+ _cli_MAX_CALLS_PER_HOUR="$MAX_CALLS_PER_HOUR"
2056
+ _CLI_MAX_CALLS_PER_HOUR=true
1725
2057
  shift 2
1726
2058
  ;;
1727
2059
  -p|--prompt)
@@ -1743,6 +2075,8 @@ while [[ $# -gt 0 ]]; do
1743
2075
  ;;
1744
2076
  -v|--verbose)
1745
2077
  VERBOSE_PROGRESS=true
2078
+ _cli_VERBOSE_PROGRESS="$VERBOSE_PROGRESS"
2079
+ _CLI_VERBOSE_PROGRESS=true
1746
2080
  shift
1747
2081
  ;;
1748
2082
  -l|--live)
@@ -1752,6 +2086,8 @@ while [[ $# -gt 0 ]]; do
1752
2086
  -t|--timeout)
1753
2087
  if [[ "$2" =~ ^[1-9][0-9]*$ ]] && [[ "$2" -le 120 ]]; then
1754
2088
  CLAUDE_TIMEOUT_MINUTES="$2"
2089
+ _cli_CLAUDE_TIMEOUT_MINUTES="$CLAUDE_TIMEOUT_MINUTES"
2090
+ _CLI_CLAUDE_TIMEOUT_MINUTES=true
1755
2091
  else
1756
2092
  echo "Error: Timeout must be a positive integer between 1 and 120 minutes"
1757
2093
  exit 1
@@ -1785,6 +2121,8 @@ while [[ $# -gt 0 ]]; do
1785
2121
  --output-format)
1786
2122
  if [[ "$2" == "json" || "$2" == "text" ]]; then
1787
2123
  CLAUDE_OUTPUT_FORMAT="$2"
2124
+ _cli_CLAUDE_OUTPUT_FORMAT="$CLAUDE_OUTPUT_FORMAT"
2125
+ _CLI_CLAUDE_OUTPUT_FORMAT=true
1788
2126
  else
1789
2127
  echo "Error: --output-format must be 'json' or 'text'"
1790
2128
  exit 1
@@ -1793,11 +2131,14 @@ while [[ $# -gt 0 ]]; do
1793
2131
  ;;
1794
2132
  --allowed-tools)
1795
2133
  CLAUDE_ALLOWED_TOOLS="$2"
2134
+ _cli_CLAUDE_ALLOWED_TOOLS="$2"
1796
2135
  _CLI_ALLOWED_TOOLS=true
1797
2136
  shift 2
1798
2137
  ;;
1799
2138
  --no-continue)
1800
2139
  CLAUDE_USE_CONTINUE=false
2140
+ _cli_CLAUDE_USE_CONTINUE="$CLAUDE_USE_CONTINUE"
2141
+ _CLI_SESSION_CONTINUITY=true
1801
2142
  shift
1802
2143
  ;;
1803
2144
  --session-expiry)
@@ -1806,6 +2147,8 @@ while [[ $# -gt 0 ]]; do
1806
2147
  exit 1
1807
2148
  fi
1808
2149
  CLAUDE_SESSION_EXPIRY_HOURS="$2"
2150
+ _cli_CLAUDE_SESSION_EXPIRY_HOURS="$2"
2151
+ _CLI_SESSION_EXPIRY_HOURS=true
1809
2152
  shift 2
1810
2153
  ;;
1811
2154
  --auto-reset-circuit)