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
@@ -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,EnterPlanMode,ExitPlanMode,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:-bypassPermissions}"
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,9 @@ VALID_TOOL_PATTERNS=(
80
107
  "TodoWrite"
81
108
  "WebFetch"
82
109
  "WebSearch"
110
+ "AskUserQuestion"
111
+ "EnterPlanMode"
112
+ "ExitPlanMode"
83
113
  "Bash"
84
114
  "Bash(git *)"
85
115
  "Bash(npm *)"
@@ -88,6 +118,8 @@ VALID_TOOL_PATTERNS=(
88
118
  "Bash(node *)"
89
119
  "NotebookEdit"
90
120
  )
121
+ ALLOWED_TOOLS_IGNORED_WARNED=false
122
+ PERMISSION_DENIAL_ACTION=""
91
123
 
92
124
  # Exit detection configuration
93
125
  EXIT_SIGNALS_FILE="$RALPH_DIR/.exit_signals"
@@ -131,7 +163,9 @@ resolve_ralphrc_file() {
131
163
  # - MAX_CALLS_PER_HOUR
132
164
  # - CLAUDE_TIMEOUT_MINUTES
133
165
  # - CLAUDE_OUTPUT_FORMAT
134
- # - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS)
166
+ # - CLAUDE_PERMISSION_MODE
167
+ # - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS for Claude Code only)
168
+ # - PERMISSION_DENIAL_MODE
135
169
  # - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
136
170
  # - SESSION_EXPIRY_HOURS (mapped to CLAUDE_SESSION_EXPIRY_HOURS)
137
171
  # - CB_NO_PROGRESS_THRESHOLD
@@ -155,6 +189,9 @@ load_ralphrc() {
155
189
  if [[ -n "${ALLOWED_TOOLS:-}" ]]; then
156
190
  CLAUDE_ALLOWED_TOOLS="$ALLOWED_TOOLS"
157
191
  fi
192
+ if [[ -n "${PERMISSION_DENIAL_MODE:-}" ]]; then
193
+ PERMISSION_DENIAL_MODE="$PERMISSION_DENIAL_MODE"
194
+ fi
158
195
  if [[ -n "${SESSION_CONTINUITY:-}" ]]; then
159
196
  CLAUDE_USE_CONTINUE="$SESSION_CONTINUITY"
160
197
  fi
@@ -167,22 +204,57 @@ load_ralphrc() {
167
204
 
168
205
  # Restore ONLY values that were explicitly set via environment variables
169
206
  # (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.
207
+ # Internal CLAUDE_* variables are kept for backward compatibility.
171
208
  [[ -n "$_env_MAX_CALLS_PER_HOUR" ]] && MAX_CALLS_PER_HOUR="$_env_MAX_CALLS_PER_HOUR"
172
209
  [[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
173
210
  [[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
174
211
  [[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
212
+ if [[ "$_env_has_CLAUDE_PERMISSION_MODE" == "x" ]]; then
213
+ CLAUDE_PERMISSION_MODE="$_env_CLAUDE_PERMISSION_MODE"
214
+ fi
175
215
  [[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
176
216
  [[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
217
+ [[ -n "$_env_PERMISSION_DENIAL_MODE" ]] && PERMISSION_DENIAL_MODE="$_env_PERMISSION_DENIAL_MODE"
177
218
  [[ -n "$_env_VERBOSE_PROGRESS" ]] && VERBOSE_PROGRESS="$_env_VERBOSE_PROGRESS"
219
+
220
+ # Public aliases are the preferred external interface and win over the
221
+ # legacy internal environment variables when both are explicitly set.
222
+ [[ -n "$_env_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_ALLOWED_TOOLS"
223
+ [[ -n "$_env_SESSION_CONTINUITY" ]] && CLAUDE_USE_CONTINUE="$_env_SESSION_CONTINUITY"
224
+ [[ -n "$_env_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_SESSION_EXPIRY_HOURS"
225
+ [[ -n "$_env_RALPH_VERBOSE" ]] && VERBOSE_PROGRESS="$_env_RALPH_VERBOSE"
226
+
227
+ # CLI flags are the highest-priority runtime inputs because they are
228
+ # parsed before main() and would otherwise be overwritten by .ralphrc.
229
+ # Keep every config-backed CLI flag here so the precedence contract stays
230
+ # consistent: CLI > public env aliases > internal env vars > config.
231
+ [[ "$_CLI_MAX_CALLS_PER_HOUR" == "true" ]] && MAX_CALLS_PER_HOUR="$_cli_MAX_CALLS_PER_HOUR"
232
+ [[ "$_CLI_CLAUDE_TIMEOUT_MINUTES" == "true" ]] && CLAUDE_TIMEOUT_MINUTES="$_cli_CLAUDE_TIMEOUT_MINUTES"
233
+ [[ "$_CLI_CLAUDE_OUTPUT_FORMAT" == "true" ]] && CLAUDE_OUTPUT_FORMAT="$_cli_CLAUDE_OUTPUT_FORMAT"
234
+ [[ "$_CLI_ALLOWED_TOOLS" == "true" ]] && CLAUDE_ALLOWED_TOOLS="$_cli_CLAUDE_ALLOWED_TOOLS"
235
+ [[ "$_CLI_SESSION_CONTINUITY" == "true" ]] && CLAUDE_USE_CONTINUE="$_cli_CLAUDE_USE_CONTINUE"
236
+ [[ "$_CLI_SESSION_EXPIRY_HOURS" == "true" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_cli_CLAUDE_SESSION_EXPIRY_HOURS"
237
+ [[ "$_CLI_VERBOSE_PROGRESS" == "true" ]] && VERBOSE_PROGRESS="$_cli_VERBOSE_PROGRESS"
178
238
  [[ -n "$_env_CB_COOLDOWN_MINUTES" ]] && CB_COOLDOWN_MINUTES="$_env_CB_COOLDOWN_MINUTES"
179
239
  [[ -n "$_env_CB_AUTO_RESET" ]] && CB_AUTO_RESET="$_env_CB_AUTO_RESET"
180
240
 
241
+ normalize_claude_permission_mode
181
242
  RALPHRC_FILE="$config_file"
182
243
  RALPHRC_LOADED=true
183
244
  return 0
184
245
  }
185
246
 
247
+ driver_supports_tool_allowlist() {
248
+ return 1
249
+ }
250
+
251
+ driver_permission_denial_help() {
252
+ echo " - Review the active driver's permission or approval settings."
253
+ echo " - ALLOWED_TOOLS in $RALPHRC_FILE only applies to the Claude Code driver."
254
+ echo " - Keep CLAUDE_PERMISSION_MODE=bypassPermissions for unattended Claude Code loops."
255
+ echo " - After updating permissions, reset the session and restart the loop."
256
+ }
257
+
186
258
  # Source platform driver
187
259
  load_platform_driver() {
188
260
  local driver_file="$SCRIPT_DIR/drivers/${PLATFORM_DRIVER}.sh"
@@ -323,8 +395,8 @@ setup_tmux_session() {
323
395
  if [[ "$CLAUDE_TIMEOUT_MINUTES" != "15" ]]; then
324
396
  ralph_cmd="$ralph_cmd --timeout $CLAUDE_TIMEOUT_MINUTES"
325
397
  fi
326
- # Forward --allowed-tools if non-default
327
- if [[ "$CLAUDE_ALLOWED_TOOLS" != "Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)" ]]; then
398
+ # Forward --allowed-tools only for drivers that support tool allowlists
399
+ if driver_supports_tool_allowlist && [[ "$CLAUDE_ALLOWED_TOOLS" != "$DEFAULT_CLAUDE_ALLOWED_TOOLS" ]]; then
328
400
  ralph_cmd="$ralph_cmd --allowed-tools '$CLAUDE_ALLOWED_TOOLS'"
329
401
  fi
330
402
  # Forward --no-continue if session continuity disabled
@@ -443,6 +515,175 @@ update_status() {
443
515
  }' > "$STATUS_FILE"
444
516
  }
445
517
 
518
+ validate_permission_denial_mode() {
519
+ local mode=$1
520
+
521
+ case "$mode" in
522
+ continue|halt|threshold)
523
+ return 0
524
+ ;;
525
+ *)
526
+ echo "Error: Invalid PERMISSION_DENIAL_MODE: '$mode'"
527
+ echo "Valid modes: continue halt threshold"
528
+ return 1
529
+ ;;
530
+ esac
531
+ }
532
+
533
+ normalize_claude_permission_mode() {
534
+ if [[ -z "${CLAUDE_PERMISSION_MODE:-}" ]]; then
535
+ CLAUDE_PERMISSION_MODE="bypassPermissions"
536
+ fi
537
+ }
538
+
539
+ validate_claude_permission_mode() {
540
+ local mode=$1
541
+
542
+ case "$mode" in
543
+ auto|acceptEdits|bypassPermissions|default|dontAsk|plan)
544
+ return 0
545
+ ;;
546
+ *)
547
+ echo "Error: Invalid CLAUDE_PERMISSION_MODE: '$mode'"
548
+ echo "Valid modes: auto acceptEdits bypassPermissions default dontAsk plan"
549
+ return 1
550
+ ;;
551
+ esac
552
+ }
553
+
554
+ warn_if_allowed_tools_ignored() {
555
+ if driver_supports_tool_allowlist; then
556
+ return 0
557
+ fi
558
+
559
+ if [[ "$ALLOWED_TOOLS_IGNORED_WARNED" == "true" ]]; then
560
+ return 0
561
+ fi
562
+
563
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" || "$CLAUDE_ALLOWED_TOOLS" != "$DEFAULT_CLAUDE_ALLOWED_TOOLS" ]]; then
564
+ log_status "WARN" "ALLOWED_TOOLS/--allowed-tools is ignored by $DRIVER_DISPLAY_NAME."
565
+ ALLOWED_TOOLS_IGNORED_WARNED=true
566
+ fi
567
+
568
+ return 0
569
+ }
570
+
571
+ show_current_allowed_tools() {
572
+ if ! driver_supports_tool_allowlist; then
573
+ return 0
574
+ fi
575
+
576
+ if [[ -f "$RALPHRC_FILE" ]]; then
577
+ local current_tools=$(grep "^ALLOWED_TOOLS=" "$RALPHRC_FILE" 2>/dev/null | cut -d= -f2- | tr -d '"')
578
+ if [[ -n "$current_tools" ]]; then
579
+ echo -e "${BLUE}Current ALLOWED_TOOLS:${NC} $current_tools"
580
+ echo ""
581
+ fi
582
+ fi
583
+
584
+ return 0
585
+ }
586
+
587
+ response_analysis_has_permission_denials() {
588
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
589
+ return 1
590
+ fi
591
+
592
+ local has_permission_denials
593
+ has_permission_denials=$(jq -r '.analysis.has_permission_denials // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
594
+
595
+ [[ "$has_permission_denials" == "true" ]]
596
+ }
597
+
598
+ get_response_analysis_denied_commands() {
599
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
600
+ echo "unknown"
601
+ return 0
602
+ fi
603
+
604
+ jq -r '.analysis.denied_commands | join(", ")' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "unknown"
605
+ }
606
+
607
+ clear_response_analysis_permission_denials() {
608
+ if [[ ! -f "$RESPONSE_ANALYSIS_FILE" ]]; then
609
+ return 0
610
+ fi
611
+
612
+ local tmp_file="$RESPONSE_ANALYSIS_FILE.tmp"
613
+ if jq '
614
+ (.analysis //= {}) |
615
+ .analysis.has_completion_signal = false |
616
+ .analysis.exit_signal = false |
617
+ .analysis.has_permission_denials = false |
618
+ .analysis.permission_denial_count = 0 |
619
+ .analysis.denied_commands = []
620
+ ' "$RESPONSE_ANALYSIS_FILE" > "$tmp_file" 2>/dev/null; then
621
+ mv "$tmp_file" "$RESPONSE_ANALYSIS_FILE"
622
+ return 0
623
+ fi
624
+
625
+ rm -f "$tmp_file" 2>/dev/null
626
+ return 1
627
+ }
628
+
629
+ handle_permission_denial() {
630
+ local loop_count=$1
631
+ local denied_cmds=${2:-unknown}
632
+ local calls_made
633
+ calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
634
+ PERMISSION_DENIAL_ACTION=""
635
+
636
+ case "$PERMISSION_DENIAL_MODE" in
637
+ continue|threshold)
638
+ log_status "WARN" "🚫 Permission denied in loop #$loop_count: $denied_cmds"
639
+ log_status "WARN" "PERMISSION_DENIAL_MODE=$PERMISSION_DENIAL_MODE - continuing execution"
640
+ update_status "$loop_count" "$calls_made" "permission_denied" "running"
641
+ PERMISSION_DENIAL_ACTION="continue"
642
+ return 0
643
+ ;;
644
+ halt)
645
+ log_status "ERROR" "🚫 Permission denied - halting loop"
646
+ reset_session "permission_denied"
647
+ update_status "$loop_count" "$calls_made" "permission_denied" "halted" "permission_denied"
648
+
649
+ echo ""
650
+ echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
651
+ echo -e "${RED}║ PERMISSION DENIED - Loop Halted ║${NC}"
652
+ echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
653
+ echo ""
654
+ echo -e "${YELLOW}$DRIVER_DISPLAY_NAME was denied permission to execute commands.${NC}"
655
+ echo ""
656
+ echo -e "${YELLOW}To fix this:${NC}"
657
+ driver_permission_denial_help
658
+ echo ""
659
+ show_current_allowed_tools
660
+ PERMISSION_DENIAL_ACTION="halt"
661
+ return 0
662
+ ;;
663
+ esac
664
+
665
+ return 1
666
+ }
667
+
668
+ consume_current_loop_permission_denial() {
669
+ local loop_count=$1
670
+ PERMISSION_DENIAL_ACTION=""
671
+
672
+ if ! response_analysis_has_permission_denials; then
673
+ return 1
674
+ fi
675
+
676
+ local denied_cmds
677
+ denied_cmds=$(get_response_analysis_denied_commands)
678
+
679
+ if ! clear_response_analysis_permission_denials; then
680
+ log_status "WARN" "Failed to clear permission denial markers from response analysis"
681
+ fi
682
+
683
+ handle_permission_denial "$loop_count" "$denied_cmds"
684
+ return 0
685
+ }
686
+
446
687
  # Check if we can make another call
447
688
  can_make_call() {
448
689
  local calls_made=0
@@ -487,6 +728,74 @@ wait_for_reset() {
487
728
  log_status "SUCCESS" "Rate limit reset! Ready for new calls."
488
729
  }
489
730
 
731
+ count_fix_plan_checkboxes() {
732
+ local fix_plan_file="${1:-$RALPH_DIR/@fix_plan.md}"
733
+ local completed_items=0
734
+ local uncompleted_items=0
735
+ local total_items=0
736
+
737
+ if [[ -f "$fix_plan_file" ]]; then
738
+ uncompleted_items=$(grep -cE "^[[:space:]]*- \[ \]" "$fix_plan_file" 2>/dev/null || true)
739
+ [[ -z "$uncompleted_items" ]] && uncompleted_items=0
740
+ completed_items=$(grep -cE "^[[:space:]]*- \[[xX]\]" "$fix_plan_file" 2>/dev/null || true)
741
+ [[ -z "$completed_items" ]] && completed_items=0
742
+ fi
743
+
744
+ total_items=$((completed_items + uncompleted_items))
745
+ printf '%s %s %s\n' "$completed_items" "$uncompleted_items" "$total_items"
746
+ }
747
+
748
+ enforce_fix_plan_progress_tracking() {
749
+ local analysis_file=$1
750
+ local completed_before=$2
751
+ local completed_after=$3
752
+
753
+ if [[ ! -f "$analysis_file" ]]; then
754
+ return 0
755
+ fi
756
+
757
+ local claimed_tasks
758
+ claimed_tasks=$(jq -r '.analysis.tasks_completed_this_loop // 0' "$analysis_file" 2>/dev/null || echo "0")
759
+ if [[ ! "$claimed_tasks" =~ ^-?[0-9]+$ ]]; then
760
+ claimed_tasks=0
761
+ fi
762
+
763
+ local fix_plan_completed_delta=$((completed_after - completed_before))
764
+ local has_progress_tracking_mismatch=false
765
+ if [[ $claimed_tasks -ne $fix_plan_completed_delta || $claimed_tasks -gt 1 || $fix_plan_completed_delta -gt 1 || $fix_plan_completed_delta -lt 0 ]]; then
766
+ has_progress_tracking_mismatch=true
767
+ fi
768
+
769
+ local tmp_file="$analysis_file.tmp"
770
+ if jq \
771
+ --argjson claimed_tasks "$claimed_tasks" \
772
+ --argjson fix_plan_completed_delta "$fix_plan_completed_delta" \
773
+ --argjson has_progress_tracking_mismatch "$has_progress_tracking_mismatch" \
774
+ '
775
+ (.analysis //= {}) |
776
+ .analysis.tasks_completed_this_loop = $claimed_tasks |
777
+ .analysis.fix_plan_completed_delta = $fix_plan_completed_delta |
778
+ .analysis.has_progress_tracking_mismatch = $has_progress_tracking_mismatch |
779
+ if $has_progress_tracking_mismatch then
780
+ .analysis.has_completion_signal = false |
781
+ .analysis.exit_signal = false
782
+ else
783
+ .
784
+ end
785
+ ' "$analysis_file" > "$tmp_file" 2>/dev/null; then
786
+ mv "$tmp_file" "$analysis_file"
787
+ else
788
+ rm -f "$tmp_file" 2>/dev/null
789
+ return 0
790
+ fi
791
+
792
+ if [[ "$has_progress_tracking_mismatch" == "true" ]]; then
793
+ log_status "WARN" "Progress tracking mismatch: claimed $claimed_tasks completed task(s) but checkbox delta was $fix_plan_completed_delta. Completion signals suppressed for this loop."
794
+ fi
795
+
796
+ return 0
797
+ }
798
+
490
799
  # Check if we should gracefully exit
491
800
  should_exit_gracefully() {
492
801
 
@@ -508,21 +817,6 @@ should_exit_gracefully() {
508
817
 
509
818
  # Check for exit conditions
510
819
 
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
820
  # 1. Too many consecutive test-only loops
527
821
  if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
528
822
  log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
@@ -568,11 +862,10 @@ should_exit_gracefully() {
568
862
  # Fix #144: Only match valid markdown checkboxes, not date entries like [2026-01-29]
569
863
  # Valid patterns: "- [ ]" (uncompleted) and "- [x]" or "- [X]" (completed)
570
864
  if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
571
- local uncompleted_items=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
572
- [[ -z "$uncompleted_items" ]] && uncompleted_items=0
573
- local completed_items=$(grep -cE "^[[:space:]]*- \[[xX]\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
574
- [[ -z "$completed_items" ]] && completed_items=0
575
- local total_items=$((uncompleted_items + completed_items))
865
+ local completed_items=0
866
+ local uncompleted_items=0
867
+ local total_items=0
868
+ read -r completed_items uncompleted_items total_items < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
576
869
 
577
870
  if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then
578
871
  log_status "WARN" "Exit condition: All @fix_plan.md items completed ($completed_items/$total_items)" >&2
@@ -677,8 +970,10 @@ build_loop_context() {
677
970
  # Extract incomplete tasks from @fix_plan.md
678
971
  # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
679
972
  if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
680
- local incomplete_tasks=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
681
- [[ -z "$incomplete_tasks" ]] && incomplete_tasks=0
973
+ local completed_tasks=0
974
+ local incomplete_tasks=0
975
+ local total_tasks=0
976
+ read -r completed_tasks incomplete_tasks total_tasks < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
682
977
  context+="Remaining tasks: ${incomplete_tasks}. "
683
978
  fi
684
979
 
@@ -810,6 +1105,7 @@ save_claude_session() {
810
1105
  session_id=$(extract_session_id_from_output "$output_file" 2>/dev/null || echo "")
811
1106
  if [[ -n "$session_id" && "$session_id" != "null" ]]; then
812
1107
  echo "$session_id" > "$CLAUDE_SESSION_FILE"
1108
+ sync_ralph_session_with_driver "$session_id"
813
1109
  log_status "INFO" "Saved session: ${session_id:0:20}..."
814
1110
  fi
815
1111
  fi
@@ -819,6 +1115,101 @@ save_claude_session() {
819
1115
  # SESSION LIFECYCLE MANAGEMENT FUNCTIONS (Phase 1.2)
820
1116
  # =============================================================================
821
1117
 
1118
+ write_active_ralph_session() {
1119
+ local session_id=$1
1120
+ local created_at=$2
1121
+ local last_used=${3:-$created_at}
1122
+
1123
+ jq -n \
1124
+ --arg session_id "$session_id" \
1125
+ --arg created_at "$created_at" \
1126
+ --arg last_used "$last_used" \
1127
+ '{
1128
+ session_id: $session_id,
1129
+ created_at: $created_at,
1130
+ last_used: $last_used
1131
+ }' > "$RALPH_SESSION_FILE"
1132
+ }
1133
+
1134
+ write_inactive_ralph_session() {
1135
+ local reset_at=$1
1136
+ local reset_reason=$2
1137
+
1138
+ jq -n \
1139
+ --arg session_id "" \
1140
+ --arg reset_at "$reset_at" \
1141
+ --arg reset_reason "$reset_reason" \
1142
+ '{
1143
+ session_id: $session_id,
1144
+ reset_at: $reset_at,
1145
+ reset_reason: $reset_reason
1146
+ }' > "$RALPH_SESSION_FILE"
1147
+ }
1148
+
1149
+ get_ralph_session_state() {
1150
+ if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
1151
+ echo "missing"
1152
+ return 0
1153
+ fi
1154
+
1155
+ if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
1156
+ echo "invalid"
1157
+ return 0
1158
+ fi
1159
+
1160
+ local session_id_type
1161
+ session_id_type=$(
1162
+ jq -r 'if has("session_id") then (.session_id | type) else "missing" end' \
1163
+ "$RALPH_SESSION_FILE" 2>/dev/null
1164
+ ) || {
1165
+ echo "invalid"
1166
+ return 0
1167
+ }
1168
+
1169
+ if [[ "$session_id_type" != "string" ]]; then
1170
+ echo "invalid"
1171
+ return 0
1172
+ fi
1173
+
1174
+ local session_id
1175
+ session_id=$(jq -r '.session_id' "$RALPH_SESSION_FILE" 2>/dev/null) || {
1176
+ echo "invalid"
1177
+ return 0
1178
+ }
1179
+
1180
+ if [[ "$session_id" == "" ]]; then
1181
+ echo "inactive"
1182
+ return 0
1183
+ fi
1184
+
1185
+ local created_at_type
1186
+ created_at_type=$(
1187
+ jq -r 'if has("created_at") then (.created_at | type) else "missing" end' \
1188
+ "$RALPH_SESSION_FILE" 2>/dev/null
1189
+ ) || {
1190
+ echo "invalid"
1191
+ return 0
1192
+ }
1193
+
1194
+ if [[ "$created_at_type" != "string" ]]; then
1195
+ echo "invalid"
1196
+ return 0
1197
+ fi
1198
+
1199
+ local created_at
1200
+ created_at=$(jq -r '.created_at' "$RALPH_SESSION_FILE" 2>/dev/null) || {
1201
+ echo "invalid"
1202
+ return 0
1203
+ }
1204
+
1205
+ if ! is_usable_ralph_session_created_at "$created_at"; then
1206
+ echo "invalid"
1207
+ return 0
1208
+ fi
1209
+
1210
+ echo "active"
1211
+ }
1212
+
822
1213
  # Get current session ID from Ralph session file
823
1214
  # Returns: session ID string or empty if not found
824
1215
  get_session_id() {
@@ -840,6 +1231,65 @@ get_session_id() {
840
1231
  return 0
841
1232
  }
842
1233
 
1234
+ is_usable_ralph_session_created_at() {
1235
+ local created_at=$1
1236
+ if [[ -z "$created_at" || "$created_at" == "null" ]]; then
1237
+ return 1
1238
+ fi
1239
+
1240
+ local created_at_epoch
1241
+ created_at_epoch=$(parse_iso_to_epoch_strict "$created_at") || return 1
1242
+
1243
+ local now_epoch
1244
+ now_epoch=$(get_epoch_seconds)
1245
+
1246
+ [[ "$created_at_epoch" -le "$now_epoch" ]]
1247
+ }
1248
+
1249
+ get_active_session_created_at() {
1250
+ if [[ "$(get_ralph_session_state)" != "active" ]]; then
1251
+ echo ""
1252
+ return 0
1253
+ fi
1254
+
1255
+ local created_at
1256
+ created_at=$(jq -r '.created_at // ""' "$RALPH_SESSION_FILE" 2>/dev/null)
1257
+ if [[ "$created_at" == "null" ]]; then
1258
+ created_at=""
1259
+ fi
1260
+
1261
+ if ! is_usable_ralph_session_created_at "$created_at"; then
1262
+ echo ""
1263
+ return 0
1264
+ fi
1265
+
1266
+ echo "$created_at"
1267
+ }
1268
+
1269
+ sync_ralph_session_with_driver() {
1270
+ local driver_session_id=$1
1271
+ if [[ -z "$driver_session_id" || "$driver_session_id" == "null" ]]; then
1272
+ return 0
1273
+ fi
1274
+
1275
+ local ts
1276
+ ts=$(get_iso_timestamp)
1277
+
1278
+ if [[ "$(get_ralph_session_state)" == "active" ]]; then
1279
+ local current_session_id
1280
+ current_session_id=$(get_session_id)
1281
+ local current_created_at
1282
+ current_created_at=$(get_active_session_created_at)
1283
+
1284
+ if [[ "$current_session_id" == "$driver_session_id" && -n "$current_created_at" ]]; then
1285
+ write_active_ralph_session "$driver_session_id" "$current_created_at" "$ts"
1286
+ return 0
1287
+ fi
1288
+ fi
1289
+
1290
+ write_active_ralph_session "$driver_session_id" "$ts" "$ts"
1291
+ }
1292
+
843
1293
  # Reset session with reason logging
844
1294
  # Usage: reset_session "reason_for_reset"
845
1295
  reset_session() {
@@ -849,20 +1299,7 @@ reset_session() {
849
1299
  local reset_timestamp
850
1300
  reset_timestamp=$(get_iso_timestamp)
851
1301
 
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"
1302
+ write_inactive_ralph_session "$reset_timestamp" "$reason"
866
1303
 
867
1304
  # Also clear the Claude session file for consistency
868
1305
  rm -f "$CLAUDE_SESSION_FILE" 2>/dev/null
@@ -951,67 +1388,39 @@ init_session_tracking() {
951
1388
  local ts
952
1389
  ts=$(get_iso_timestamp)
953
1390
 
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)"
1391
+ local session_state
1392
+ session_state=$(get_ralph_session_state)
1393
+ if [[ "$session_state" == "active" ]]; then
974
1394
  return 0
975
1395
  fi
976
1396
 
977
- # Validate existing session file
978
- if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
1397
+ if [[ "$session_state" == "invalid" ]]; then
979
1398
  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
1399
  fi
1400
+
1401
+ local new_session_id
1402
+ new_session_id=$(generate_session_id)
1403
+ write_active_ralph_session "$new_session_id" "$ts" "$ts"
1404
+
1405
+ log_status "INFO" "Initialized session tracking (session: $new_session_id)"
997
1406
  }
998
1407
 
999
1408
  # Update last_used timestamp in session file (called on each loop iteration)
1000
1409
  update_session_last_used() {
1001
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
1410
+ if [[ "$(get_ralph_session_state)" != "active" ]]; then
1002
1411
  return 0
1003
1412
  fi
1004
1413
 
1005
1414
  local ts
1006
1415
  ts=$(get_iso_timestamp)
1007
1416
 
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=$?
1417
+ local session_id
1418
+ session_id=$(get_session_id)
1419
+ local created_at
1420
+ created_at=$(get_active_session_created_at)
1012
1421
 
1013
- if [[ $jq_status -eq 0 && -n "$updated" ]]; then
1014
- echo "$updated" > "$RALPH_SESSION_FILE"
1422
+ if [[ -n "$session_id" && -n "$created_at" ]]; then
1423
+ write_active_ralph_session "$session_id" "$created_at" "$ts"
1015
1424
  fi
1016
1425
  }
1017
1426
 
@@ -1072,6 +1481,8 @@ execute_claude_code() {
1072
1481
  local loop_count=$1
1073
1482
  local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1074
1483
  calls_made=$((calls_made + 1))
1484
+ local fix_plan_completed_before=0
1485
+ read -r fix_plan_completed_before _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1075
1486
 
1076
1487
  # Fix #141: Capture git HEAD SHA at loop start to detect commits as progress
1077
1488
  # Store in file for access by progress detection after Claude execution
@@ -1326,6 +1737,10 @@ EOF
1326
1737
  analyze_response "$output_file" "$loop_count"
1327
1738
  local analysis_exit_code=$?
1328
1739
 
1740
+ local fix_plan_completed_after=0
1741
+ read -r fix_plan_completed_after _ _ < <(count_fix_plan_checkboxes "$RALPH_DIR/@fix_plan.md")
1742
+ enforce_fix_plan_progress_tracking "$RESPONSE_ANALYSIS_FILE" "$fix_plan_completed_before" "$fix_plan_completed_after"
1743
+
1329
1744
  # Update exit signals based on analysis
1330
1745
  update_exit_signals
1331
1746
 
@@ -1435,11 +1850,31 @@ loop_count=0
1435
1850
  main() {
1436
1851
  initialize_runtime_context
1437
1852
 
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
1853
+ if ! validate_permission_denial_mode "$PERMISSION_DENIAL_MODE"; then
1440
1854
  exit 1
1441
1855
  fi
1442
1856
 
1857
+ if [[ "$(driver_name)" == "claude-code" ]]; then
1858
+ normalize_claude_permission_mode
1859
+
1860
+ if ! validate_claude_permission_mode "$CLAUDE_PERMISSION_MODE"; then
1861
+ exit 1
1862
+ fi
1863
+ fi
1864
+
1865
+ if driver_supports_tool_allowlist; then
1866
+ # Validate --allowed-tools now that platform-specific VALID_TOOL_PATTERNS are loaded
1867
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! validate_allowed_tools "$CLAUDE_ALLOWED_TOOLS"; then
1868
+ exit 1
1869
+ fi
1870
+ else
1871
+ warn_if_allowed_tools_ignored
1872
+ fi
1873
+
1874
+ if [[ "${_CLI_ALLOWED_TOOLS:-}" == "true" ]] && ! driver_supports_tool_allowlist; then
1875
+ _CLI_ALLOWED_TOOLS=""
1876
+ fi
1877
+
1443
1878
  log_status "SUCCESS" "🚀 Ralph loop starting with $DRIVER_DISPLAY_NAME"
1444
1879
  log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
1445
1880
  log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
@@ -1531,45 +1966,6 @@ main() {
1531
1966
  # Check for graceful exit conditions
1532
1967
  local exit_reason=$(should_exit_gracefully)
1533
1968
  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
1969
  log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
1574
1970
  reset_session "project_complete"
1575
1971
  update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
@@ -1591,6 +1987,17 @@ main() {
1591
1987
  local exec_result=$?
1592
1988
 
1593
1989
  if [ $exec_result -eq 0 ]; then
1990
+ if consume_current_loop_permission_denial "$loop_count"; then
1991
+ if [[ "$PERMISSION_DENIAL_ACTION" == "halt" ]]; then
1992
+ break
1993
+ fi
1994
+
1995
+ # Brief pause between loops when the denial was recorded but
1996
+ # policy allows Ralph to continue.
1997
+ sleep 5
1998
+ continue
1999
+ fi
2000
+
1594
2001
  update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
1595
2002
 
1596
2003
  # Brief pause between successful executions
@@ -1676,7 +2083,7 @@ Options:
1676
2083
  Modern CLI Options (Phase 1.1):
1677
2084
  --output-format FORMAT Set driver output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
1678
2085
  Note: --live mode requires JSON and will auto-switch
1679
- --allowed-tools TOOLS Comma-separated list of allowed tools (default: $CLAUDE_ALLOWED_TOOLS)
2086
+ --allowed-tools TOOLS Claude Code only. Ignored by codex, cursor, and copilot
1680
2087
  --no-continue Disable session continuity across loops
1681
2088
  --session-expiry HOURS Set session expiration time in hours (default: $CLAUDE_SESSION_EXPIRY_HOURS)
1682
2089
 
@@ -1722,6 +2129,8 @@ while [[ $# -gt 0 ]]; do
1722
2129
  ;;
1723
2130
  -c|--calls)
1724
2131
  MAX_CALLS_PER_HOUR="$2"
2132
+ _cli_MAX_CALLS_PER_HOUR="$MAX_CALLS_PER_HOUR"
2133
+ _CLI_MAX_CALLS_PER_HOUR=true
1725
2134
  shift 2
1726
2135
  ;;
1727
2136
  -p|--prompt)
@@ -1743,6 +2152,8 @@ while [[ $# -gt 0 ]]; do
1743
2152
  ;;
1744
2153
  -v|--verbose)
1745
2154
  VERBOSE_PROGRESS=true
2155
+ _cli_VERBOSE_PROGRESS="$VERBOSE_PROGRESS"
2156
+ _CLI_VERBOSE_PROGRESS=true
1746
2157
  shift
1747
2158
  ;;
1748
2159
  -l|--live)
@@ -1752,6 +2163,8 @@ while [[ $# -gt 0 ]]; do
1752
2163
  -t|--timeout)
1753
2164
  if [[ "$2" =~ ^[1-9][0-9]*$ ]] && [[ "$2" -le 120 ]]; then
1754
2165
  CLAUDE_TIMEOUT_MINUTES="$2"
2166
+ _cli_CLAUDE_TIMEOUT_MINUTES="$CLAUDE_TIMEOUT_MINUTES"
2167
+ _CLI_CLAUDE_TIMEOUT_MINUTES=true
1755
2168
  else
1756
2169
  echo "Error: Timeout must be a positive integer between 1 and 120 minutes"
1757
2170
  exit 1
@@ -1785,6 +2198,8 @@ while [[ $# -gt 0 ]]; do
1785
2198
  --output-format)
1786
2199
  if [[ "$2" == "json" || "$2" == "text" ]]; then
1787
2200
  CLAUDE_OUTPUT_FORMAT="$2"
2201
+ _cli_CLAUDE_OUTPUT_FORMAT="$CLAUDE_OUTPUT_FORMAT"
2202
+ _CLI_CLAUDE_OUTPUT_FORMAT=true
1788
2203
  else
1789
2204
  echo "Error: --output-format must be 'json' or 'text'"
1790
2205
  exit 1
@@ -1793,11 +2208,14 @@ while [[ $# -gt 0 ]]; do
1793
2208
  ;;
1794
2209
  --allowed-tools)
1795
2210
  CLAUDE_ALLOWED_TOOLS="$2"
2211
+ _cli_CLAUDE_ALLOWED_TOOLS="$2"
1796
2212
  _CLI_ALLOWED_TOOLS=true
1797
2213
  shift 2
1798
2214
  ;;
1799
2215
  --no-continue)
1800
2216
  CLAUDE_USE_CONTINUE=false
2217
+ _cli_CLAUDE_USE_CONTINUE="$CLAUDE_USE_CONTINUE"
2218
+ _CLI_SESSION_CONTINUITY=true
1801
2219
  shift
1802
2220
  ;;
1803
2221
  --session-expiry)
@@ -1806,6 +2224,8 @@ while [[ $# -gt 0 ]]; do
1806
2224
  exit 1
1807
2225
  fi
1808
2226
  CLAUDE_SESSION_EXPIRY_HOURS="$2"
2227
+ _cli_CLAUDE_SESSION_EXPIRY_HOURS="$2"
2228
+ _CLI_SESSION_EXPIRY_HOURS=true
1809
2229
  shift 2
1810
2230
  ;;
1811
2231
  --auto-reset-circuit)