ralph.rb 1.2.4355354345 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/gem-push.yml +2 -2
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +53 -0
  5. data/lib/ralph/cli.rb +67 -186
  6. data/lib/ralph/display.rb +105 -0
  7. data/lib/ralph/events.rb +117 -0
  8. data/lib/ralph/loop.rb +113 -170
  9. data/lib/ralph/metrics.rb +88 -0
  10. data/lib/ralph/opencode.rb +66 -0
  11. data/lib/ralph/version.rb +1 -1
  12. data/lib/ralph.rb +0 -3
  13. data/plans/00-complete-implementation.md +120 -0
  14. data/plans/01-cli-implementation.md +53 -0
  15. data/plans/02-loop-implementation.md +78 -0
  16. data/plans/03-agents-implementation.md +76 -0
  17. data/plans/04-metrics-implementation.md +98 -0
  18. data/plans/README.md +63 -0
  19. data/specs/README.md +4 -15
  20. data/specs/__templates__/API_TEMPLATE.md +0 -0
  21. data/specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md +0 -0
  22. data/specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md +0 -0
  23. data/specs/__templates__/CONTROLLER_TEMPLATE.md +32 -0
  24. data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
  25. data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
  26. data/specs/agents.md +426 -120
  27. data/specs/cli.md +11 -218
  28. data/specs/lib/todo_item.rb +144 -0
  29. data/specs/log +15 -0
  30. data/specs/loop.md +42 -0
  31. data/specs/metrics.md +51 -0
  32. metadata +23 -39
  33. data/lib/ralph/agents/base.rb +0 -132
  34. data/lib/ralph/agents/claude_code.rb +0 -24
  35. data/lib/ralph/agents/codex.rb +0 -25
  36. data/lib/ralph/agents/open_code.rb +0 -30
  37. data/lib/ralph/agents.rb +0 -24
  38. data/lib/ralph/config.rb +0 -40
  39. data/lib/ralph/git/file_snapshot.rb +0 -60
  40. data/lib/ralph/helpers.rb +0 -76
  41. data/lib/ralph/iteration.rb +0 -220
  42. data/lib/ralph/output/active_loop_error.rb +0 -13
  43. data/lib/ralph/output/banner.rb +0 -29
  44. data/lib/ralph/output/completion_deferred.rb +0 -12
  45. data/lib/ralph/output/completion_detected.rb +0 -17
  46. data/lib/ralph/output/config_summary.rb +0 -31
  47. data/lib/ralph/output/context_consumed.rb +0 -11
  48. data/lib/ralph/output/iteration.rb +0 -45
  49. data/lib/ralph/output/max_iterations_reached.rb +0 -16
  50. data/lib/ralph/output/no_plugin_warning.rb +0 -14
  51. data/lib/ralph/output/nonzero_exit_warning.rb +0 -11
  52. data/lib/ralph/output/plugin_error.rb +0 -12
  53. data/lib/ralph/output/status.rb +0 -176
  54. data/lib/ralph/output/struggle_warning.rb +0 -18
  55. data/lib/ralph/output/task_completion.rb +0 -12
  56. data/lib/ralph/output/tasks_file_created.rb +0 -11
  57. data/lib/ralph/prompt_template.rb +0 -183
  58. data/lib/ralph/storage/context.rb +0 -58
  59. data/lib/ralph/storage/history.rb +0 -117
  60. data/lib/ralph/storage/state.rb +0 -178
  61. data/lib/ralph/storage/tasks.rb +0 -244
  62. data/lib/ralph/threads/heartbeat.rb +0 -44
  63. data/lib/ralph/threads/stream_reader.rb +0 -50
  64. data/original/bin/ralph.js +0 -13
  65. data/original/ralph.ts +0 -1706
  66. data/specs/iteration.md +0 -173
  67. data/specs/output.md +0 -104
  68. data/specs/storage/local-data-structure.md +0 -246
  69. data/specs/tasks.md +0 -295
@@ -1,220 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- class Iteration
5
- # Statuses:
6
- # :completed — completion promise detected, iteration succeeded
7
- # :continuing — no completion detected, keep looping
8
- # :failed — non-zero exit code from the agent process
9
- # :fatal — fatal error detected in agent output (unrecoverable)
10
- # :error — iteration raised an exception
11
- class Result
12
- STATUSES = %i[completed continuing failed fatal error].freeze
13
-
14
- attr_reader :status, :agent_result, :duration_ms, :files_modified, :completion_detected, :errors
15
-
16
- def initialize(status:, agent_result:, duration_ms:, files_modified:, completion_detected:, errors:)
17
- @status = status
18
- @agent_result = agent_result
19
- @duration_ms = duration_ms
20
- @files_modified = files_modified
21
- @completion_detected = completion_detected
22
- @errors = errors
23
- end
24
-
25
- def exit_code = agent_result&.exit_code
26
- def stdout_text = agent_result&.stdout_text || ""
27
- def stderr_text = agent_result&.stderr_text || ""
28
- def tool_counts = agent_result&.tool_counts || {}
29
- def combined_output = agent_result&.combined_output || ""
30
-
31
- def completed? = status == :completed
32
- def continuing? = status == :continuing
33
- def failed? = status == :failed
34
- def fatal? = status == :fatal
35
- def error? = status == :error
36
- end
37
-
38
- include ::Ralph::Helpers
39
-
40
- attr_reader :struggle_indicators, :iteration_start
41
-
42
- def initialize(loop_context)
43
- @loop = loop_context
44
- @config = @loop.config
45
- @agent = @loop.agent
46
- @state = @loop.state
47
- @struggle_indicators = @loop.struggle_indicators
48
-
49
- # Streaming configuration
50
- @compact_tools = !@config.verbose_tools
51
- @tool_summary_interval_ms = 3000
52
- @heartbeat_interval_ms = 10_000
53
-
54
- @stream_tool_counts = Hash.new(0)
55
- @mutex = Mutex.new
56
- @timing = { last_printed_at: now_ms, last_activity_at: now_ms }
57
- @last_tool_summary_at = 0
58
-
59
- @iteration_start = now_ms
60
- end
61
-
62
- def context_at_start = @_context_at_start ||= @loop.context
63
-
64
- def run
65
- snapshot_before = Git::FileSnapshot.capture
66
-
67
- if @config.stream_output
68
- heartbeat = Threads::Heartbeat.new(iteration_start, @heartbeat_interval_ms, @timing, @mutex)
69
- end
70
-
71
- @agent.execute(
72
- @loop.prompt.build_iteration(@state, @agent),
73
- on_line: method(:handle_line),
74
- model: @config.model,
75
- stream_output: @config.stream_output,
76
- disable_plugins: @config.disable_plugins,
77
- allow_all_permissions: @config.allow_all_permissions,
78
- ).then do |agent_result|
79
- heartbeat&.stop
80
-
81
- if @config.stream_output
82
- @mutex.synchronize { maybe_print_tool_summary(force: true) }
83
- end
84
-
85
- unless @config.stream_output
86
- if agent_result
87
- warn agent_result.stderr_text unless agent_result.stderr_text.empty?
88
- puts agent_result.stdout_text
89
- end
90
- end
91
-
92
- snapshot_after = Git::FileSnapshot.capture
93
-
94
- combined_output = agent_result.combined_output
95
- completion_detected = check_completion(combined_output, @config.completion_promise)
96
- fatal_error = @agent.detect_fatal_error(combined_output)
97
-
98
- status = (
99
- if fatal_error
100
- :fatal
101
- elsif agent_result.exit_code != 0
102
- :failed
103
- elsif completion_detected
104
- :completed
105
- else
106
- :continuing
107
- end
108
- )
109
-
110
- Result.new(
111
- status: status,
112
- agent_result: agent_result,
113
- duration_ms: now_ms - iteration_start,
114
- files_modified: snapshot_before.modified_since(snapshot_after),
115
- completion_detected: completion_detected,
116
- errors: @agent.extract_errors(combined_output)
117
- ).tap do |result|
118
- update_struggle_indicators(result)
119
- end
120
- end
121
- rescue StandardError => error
122
- if @config.current_pid
123
- begin
124
- Process.kill('TERM', @config.current_pid)
125
- rescue StandardError
126
- # process may have exited
127
- end
128
- @config.current_pid = nil
129
- end
130
-
131
- Output::Iteration::Error.call(@loop, error)
132
-
133
- sleep 2
134
-
135
- Result.new(
136
- status: :error,
137
- agent_result: nil,
138
- duration_ms: now_ms - (iteration_start || now_ms), # this is so fucking wrong
139
- files_modified: [],
140
- completion_detected: false,
141
- errors: [error.message]
142
- )
143
- end
144
-
145
- # ---------- Warnings and error detection ----------
146
-
147
- def handle_iteration_error(error, iteration_start)
148
- end
149
-
150
- # Returns true when the agent appears to be stuck.
151
- # Should only be called after iteration > 2 for meaningful results.
152
- def struggling?
153
- @struggle_indicators[:no_progress_iterations] >= 3 ||
154
- @struggle_indicators[:short_iterations] >= 3
155
- end
156
-
157
- private
158
-
159
- # ---------- Struggle tracking ----------
160
-
161
- def update_struggle_indicators(result)
162
- if result.files_modified.empty?
163
- @struggle_indicators[:no_progress_iterations] += 1
164
- else
165
- @struggle_indicators[:no_progress_iterations] = 0
166
- end
167
-
168
- if result.duration_ms < 30_000
169
- @struggle_indicators[:short_iterations] += 1
170
- else
171
- @struggle_indicators[:short_iterations] = 0
172
- end
173
-
174
- if result.errors.empty?
175
- @struggle_indicators[:repeated_errors] = {}
176
- else
177
- result.errors.each do |error|
178
- key = error[0, 100]
179
- @struggle_indicators[:repeated_errors][key] = (@struggle_indicators[:repeated_errors][key] || 0) + 1
180
- end
181
- end
182
- end
183
-
184
- # ---------- Agent execution ----------
185
-
186
- def maybe_print_tool_summary(force: false)
187
- if @compact_tools && @stream_tool_counts.any?
188
- now = now_ms
189
- if force || (now - @last_tool_summary_at >= @tool_summary_interval_ms)
190
- format_tool_summary(@stream_tool_counts).then do |summary|
191
- unless summary.empty?
192
- puts "| Tools #{summary}"
193
- @timing[:last_printed_at] = now_ms
194
- @last_tool_summary_at = now_ms
195
- end
196
- end
197
- end
198
- end
199
- end
200
-
201
- def handle_line(line, is_error, tool)
202
- @mutex.synchronize { @timing[:last_activity_at] = now_ms }
203
-
204
- @mutex.synchronize { @stream_tool_counts[tool] += 1 } if tool
205
-
206
- if tool && @compact_tools
207
- @mutex.synchronize { maybe_print_tool_summary }
208
- else
209
- if line.empty?
210
- puts ''
211
- elsif is_error
212
- warn line
213
- else
214
- puts line
215
- end
216
- @mutex.synchronize { @timing[:last_printed_at] = now_ms }
217
- end
218
- end
219
- end
220
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class ActiveLoopError
6
- def self.call(existing_state, path:)
7
- $stderr.puts "Error: A Ralph loop is already active (iteration #{existing_state.iteration})"
8
- $stderr.puts "Started at: #{existing_state.started_at}"
9
- $stderr.puts "To cancel it, press Ctrl+C in its terminal or delete #{path}"
10
- end
11
- end
12
- end
13
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class Banner
6
- def self.call(loop_context)
7
- Output::NoPluginWarning.call(loop_context) if loop_context.config.disable_plugins
8
-
9
- puts <<~BANNER
10
-
11
- ╔#{'=' * 66}╗
12
- ║ Ralph Wiggum Loop ║
13
- ║ Iterative AI Development with #{loop_context.agent.config_name.ljust(20)} ║
14
- ╚#{'=' * 66}╝
15
- BANNER
16
-
17
- if loop_context.config.tasks_mode
18
- tasks_storage = Storage::Tasks.new
19
- unless File.exist?(tasks_storage.path)
20
- tasks_storage.initialize_tasks_file
21
- Output::TasksFileCreated.call(path: tasks_storage.path)
22
- end
23
- end
24
-
25
- Output::ConfigSummary.call(loop_context)
26
- end
27
- end
28
- end
29
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class CompletionDeferred
6
- def self.call(config:, next_iteration:)
7
- puts "\n⏳ Completion promise detected, but minimum iterations (#{config.min_iterations}) not yet reached."
8
- puts " Continuing to iteration #{next_iteration}..."
9
- end
10
- end
11
- end
12
- end
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class CompletionDetected
6
- extend ::Ralph::Helpers
7
-
8
- def self.call(loop_context)
9
- puts "\n╔#{'=' * 66}╗"
10
- puts "║ ✅ Completion promise detected: <promise>#{loop_context.config.completion_promise}</promise>"
11
- puts "║ Task completed in #{loop_context.state.iteration} iteration(s)"
12
- puts "║ Total time: #{format_duration_long(loop_context.history.total_duration_ms)}"
13
- puts "╚#{'=' * 66}╝"
14
- end
15
- end
16
- end
17
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class ConfigSummary
6
- def self.call(loop_context)
7
- config = loop_context.config
8
- agent = loop_context.agent
9
- prompt = loop_context.prompt
10
-
11
- prompt_text = prompt.to_s
12
- prompt_preview = prompt_text.gsub(/\s+/, ' ')[0, 80] + (prompt_text.length > 80 ? '...' : '')
13
- puts "Task: #{prompt_preview}"
14
- puts "Completion promise: #{config.completion_promise}"
15
- if config.tasks_mode
16
- puts 'Tasks mode: ENABLED'
17
- puts "Task promise: #{config.task_promise}"
18
- end
19
- puts "Min iterations: #{config.min_iterations}"
20
- puts "Max iterations: #{config.max_iterations > 0 ? config.max_iterations : 'unlimited'}"
21
- puts "Agent: #{agent.config_name}"
22
- puts "Model: #{config.model}" if config.model && !config.model.empty?
23
- puts 'OpenCode plugins: non-auth plugins disabled' if config.disable_plugins && agent.type == :opencode
24
- puts 'Permissions: auto-approve all tools' if config.allow_all_permissions
25
- puts ''
26
- puts 'Starting loop... (Ctrl+C to stop)'
27
- puts '═' * 68
28
- end
29
- end
30
- end
31
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class ContextConsumed
6
- def self.call
7
- puts "📝 Context was consumed this iteration"
8
- end
9
- end
10
- end
11
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- module Iteration
6
- class Header
7
- def self.call(loop_context)
8
- config = loop_context.config
9
- iteration = loop_context.state.iteration
10
-
11
- iter_info = config.max_iterations > 0 ? " / #{config.max_iterations}" : ''
12
- min_info = config.min_iterations > 1 && iteration < config.min_iterations ? " (min: #{config.min_iterations})" : ''
13
- puts "\n🔄 Iteration #{iteration}#{iter_info}#{min_info}"
14
- puts '─' * 68
15
- end
16
- end
17
-
18
- class Summary
19
- extend ::Ralph::Helpers
20
-
21
- def self.call(loop_context, result)
22
- tool_summary = format_tool_summary(result.tool_counts)
23
- puts "\nIteration Summary"
24
- puts "─" * 68
25
- puts "Iteration: #{loop_context.state.iteration}"
26
- puts "Elapsed: #{format_duration(result.duration_ms)}"
27
- if tool_summary && !tool_summary.empty?
28
- puts "Tools: #{tool_summary}"
29
- else
30
- puts "Tools: none"
31
- end
32
- puts "Exit code: #{result.exit_code}"
33
- puts "Completion promise: #{result.completion_detected ? "detected" : "not detected"}"
34
- end
35
- end
36
-
37
- class Error
38
- def self.call(loop_context, error)
39
- $stderr.puts "\n❌ Error in iteration #{loop_context.state.iteration}: #{error}"
40
- puts "Continuing to next iteration..."
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class MaxIterationsReached
6
- extend ::Ralph::Helpers
7
-
8
- def self.call(loop_context)
9
- puts "\n╔#{'=' * 66}╗"
10
- puts "║ Max iterations (#{loop_context.config.max_iterations}) reached. Loop stopped."
11
- puts "║ Total time: #{format_duration_long(loop_context.history.total_duration_ms)}"
12
- puts "╚#{'=' * 66}╝"
13
- end
14
- end
15
- end
16
- end
@@ -1,14 +0,0 @@
1
- module Ralph
2
- module Output
3
- class NoPluginWarning
4
- def self.call(loop_context)
5
- case loop_context.agent.type
6
- when :claude_code
7
- warn 'Warning: --no-plugins has no effect with Claude Code agent'
8
- when :codex
9
- warn 'Warning: --no-plugins has no effect with Codex agent'
10
- end
11
- end
12
- end
13
- end
14
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class NonzeroExitWarning
6
- def self.call(loop_context, result)
7
- warn "\n⚠️ #{loop_context.agent.config_name} exited with code #{result.exit_code}. Continuing to next iteration."
8
- end
9
- end
10
- end
11
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class PluginError
6
- def self.call
7
- $stderr.puts "\n❌ OpenCode tried to load the legacy 'ralph-wiggum' plugin. This package is CLI-only."
8
- $stderr.puts "Remove 'ralph-wiggum' from your opencode.json plugin list, or re-run with --no-plugins."
9
- end
10
- end
11
- end
12
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class Status
6
- include ::Ralph::Helpers
7
-
8
- def self.call(options:)
9
- new(options).call
10
- end
11
-
12
- def initialize(options)
13
- @options = options
14
- end
15
-
16
- def call
17
- state = Storage::State.load
18
- history = Storage::History.load_history
19
- context = Storage::Context.new
20
- show_tasks = @options[:tasks_mode] || state&.tasks_mode
21
-
22
- print_header
23
- print_loop_status(state)
24
- print_pending_context(context)
25
- print_tasks if show_tasks
26
- print_history(history)
27
- print_footer
28
- end
29
-
30
- private
31
-
32
- def print_header
33
- puts <<~HEADER
34
-
35
- ╔#{"=" * 66}╗
36
- ║ Ralph Wiggum Status ║
37
- ╚#{"=" * 66}╝
38
- HEADER
39
- end
40
-
41
- def print_loop_status(state)
42
- if state&.active
43
- print_active_loop(state)
44
- else
45
- puts "⏹️ No active loop"
46
- end
47
- end
48
-
49
- def print_active_loop(state)
50
- elapsed = now_ms - (Time.parse(state.started_at).to_f * 1000).to_i
51
- elapsed_str = format_duration_long(elapsed)
52
- puts "🔄 ACTIVE LOOP"
53
- max_str = state.max_iterations > 0 ? " / #{state.max_iterations}" : " (unlimited)"
54
- puts " Iteration: #{state.iteration}#{max_str}"
55
- puts " Started: #{state.started_at}"
56
- puts " Elapsed: #{elapsed_str}"
57
- puts " Promise: #{state.completion_promise}"
58
- agent_label =
59
- if state.agent
60
- cfg = Agents.resolve(state.agent)
61
- cfg ? cfg.config_name : state.agent
62
- else
63
- "OpenCode"
64
- end
65
- puts " Agent: #{agent_label}"
66
- puts " Model: #{state.model}" if state.model && !state.model.empty?
67
- if state.tasks_mode
68
- puts " Tasks Mode: ENABLED"
69
- puts " Task Promise: #{state.task_promise}"
70
- end
71
- prompt_preview = state.prompt[0, 60] + (state.prompt.length > 60 ? "..." : "")
72
- puts " Prompt: #{prompt_preview}"
73
- end
74
-
75
- def print_pending_context(context)
76
- return unless context.present?
77
-
78
- puts "\n📝 PENDING CONTEXT (will be injected next iteration):"
79
- puts " #{context.content.split("\n").join("\n ")}"
80
- end
81
-
82
- def print_tasks
83
- tasks_storage = Storage::Tasks.new
84
- if File.exist?(tasks_storage.path)
85
- begin
86
- tasks_content = File.read(tasks_storage.path)
87
- tasks = Tasks.parse(tasks_content)
88
- print_current_tasks(tasks)
89
- rescue StandardError
90
- puts "\n📋 CURRENT TASKS: (error reading tasks)"
91
- end
92
- else
93
- puts "\n📋 CURRENT TASKS: (no tasks file found)"
94
- end
95
- end
96
-
97
- def print_current_tasks(tasks)
98
- if tasks.any?
99
- puts "\n📋 CURRENT TASKS:"
100
- tasks.each_with_index do |task, i|
101
- icon = tasks.status_icon(task.status)
102
- puts " #{i + 1}. #{icon} #{task.text}"
103
- task.subtasks.each do |subtask|
104
- sub_icon = tasks.status_icon(subtask.status)
105
- puts " #{sub_icon} #{subtask.text}"
106
- end
107
- end
108
- complete = tasks.count { |t| t.status == :complete }
109
- in_progress = tasks.count { |t| t.status == :in_progress }
110
- puts "\n Progress: #{complete}/#{tasks.length} complete, #{in_progress} in progress"
111
- else
112
- puts "\n📋 CURRENT TASKS: (no tasks found)"
113
- end
114
- end
115
-
116
- def print_history(history)
117
- iterations = history["iterations"] || []
118
- return unless iterations.any?
119
-
120
- puts "\n📊 HISTORY (#{iterations.length} iterations)"
121
- puts " Total time: #{format_duration_long(history["total_duration_ms"] || 0)}"
122
-
123
- recent = iterations.last(5)
124
- print_recent_iterations(recent)
125
-
126
- si = history["struggle_indicators"] || {}
127
- has_repeated = (si["repeated_errors"] || {}).values.any? { |c| c >= 2 }
128
- if (si["no_progress_iterations"] || 0) >= 3 || (si["short_iterations"] || 0) >= 3 || has_repeated
129
- print_struggle_warnings(si)
130
- end
131
- end
132
-
133
- def print_recent_iterations(iterations)
134
- puts "\n Recent iterations:"
135
- iterations.each do |iter|
136
- tools = (iter["tools_used"] || {})
137
- .sort_by { |_, v| -v }
138
- .first(3)
139
- .map { |k, v| "#{k}:#{v}" }
140
- .join(" ")
141
- status_icon =
142
- if iter["completion_detected"]
143
- "✅"
144
- elsif iter["exit_code"] != 0
145
- "❌"
146
- else
147
- "🔄"
148
- end
149
- puts " #{status_icon} ##{iter["iteration"]}: #{format_duration_long(iter["duration_ms"])} | #{tools.empty? ? "no tools" : tools}"
150
- end
151
- end
152
-
153
- def print_struggle_warnings(si)
154
- puts "\n⚠️ STRUGGLE INDICATORS:"
155
- if (si["no_progress_iterations"] || 0) >= 3
156
- puts " - No file changes in #{si["no_progress_iterations"]} iterations"
157
- end
158
- if (si["short_iterations"] || 0) >= 3
159
- puts " - #{si["short_iterations"]} very short iterations (< 30s)"
160
- end
161
- top_errors = (si["repeated_errors"] || {})
162
- .select { |_, count| count >= 2 }
163
- .sort_by { |_, count| -count }
164
- .first(3)
165
- top_errors.each do |error, count|
166
- puts " - Same error #{count}x: \"#{error[0, 50]}...\""
167
- end
168
- puts "\n 💡 Consider using: ralph --add-context \"your hint here\""
169
- end
170
-
171
- def print_footer
172
- puts ""
173
- end
174
- end
175
- end
176
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class StruggleWarning
6
- def self.call(loop_context)
7
- puts "\n⚠️ Potential struggle detected:"
8
- if loop_context.struggle_indicators[:no_progress_iterations] >= 3
9
- puts " - No file changes in #{loop_context.struggle_indicators[:no_progress_iterations]} iterations"
10
- end
11
- if loop_context.struggle_indicators[:short_iterations] >= 3
12
- puts " - #{loop_context.struggle_indicators[:short_iterations]} very short iterations"
13
- end
14
- puts " 💡 Tip: Use 'ralph --add-context \"hint\"' in another terminal to guide the agent"
15
- end
16
- end
17
- end
18
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class TaskCompletion
6
- def self.call(loop_context)
7
- puts "\n🔄 Task completion detected: <promise>#{loop_context.config.task_promise}</promise>"
8
- puts " Moving to next task in iteration #{loop_context.state.iteration + 1}..."
9
- end
10
- end
11
- end
12
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ralph
4
- module Output
5
- class TasksFileCreated
6
- def self.call(path:)
7
- puts "📋 Created tasks file: #{path}"
8
- end
9
- end
10
- end
11
- end