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.
- checksums.yaml +4 -4
- data/.github/workflows/gem-push.yml +2 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +53 -0
- data/lib/ralph/cli.rb +67 -186
- data/lib/ralph/display.rb +105 -0
- data/lib/ralph/events.rb +117 -0
- data/lib/ralph/loop.rb +113 -170
- data/lib/ralph/metrics.rb +88 -0
- data/lib/ralph/opencode.rb +66 -0
- data/lib/ralph/version.rb +1 -1
- data/lib/ralph.rb +0 -3
- data/plans/00-complete-implementation.md +120 -0
- data/plans/01-cli-implementation.md +53 -0
- data/plans/02-loop-implementation.md +78 -0
- data/plans/03-agents-implementation.md +76 -0
- data/plans/04-metrics-implementation.md +98 -0
- data/plans/README.md +63 -0
- data/specs/README.md +4 -15
- data/specs/__templates__/API_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md +0 -0
- data/specs/__templates__/CONTROLLER_TEMPLATE.md +32 -0
- data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
- data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
- data/specs/agents.md +426 -120
- data/specs/cli.md +11 -218
- data/specs/lib/todo_item.rb +144 -0
- data/specs/log +15 -0
- data/specs/loop.md +42 -0
- data/specs/metrics.md +51 -0
- metadata +23 -39
- data/lib/ralph/agents/base.rb +0 -132
- data/lib/ralph/agents/claude_code.rb +0 -24
- data/lib/ralph/agents/codex.rb +0 -25
- data/lib/ralph/agents/open_code.rb +0 -30
- data/lib/ralph/agents.rb +0 -24
- data/lib/ralph/config.rb +0 -40
- data/lib/ralph/git/file_snapshot.rb +0 -60
- data/lib/ralph/helpers.rb +0 -76
- data/lib/ralph/iteration.rb +0 -220
- data/lib/ralph/output/active_loop_error.rb +0 -13
- data/lib/ralph/output/banner.rb +0 -29
- data/lib/ralph/output/completion_deferred.rb +0 -12
- data/lib/ralph/output/completion_detected.rb +0 -17
- data/lib/ralph/output/config_summary.rb +0 -31
- data/lib/ralph/output/context_consumed.rb +0 -11
- data/lib/ralph/output/iteration.rb +0 -45
- data/lib/ralph/output/max_iterations_reached.rb +0 -16
- data/lib/ralph/output/no_plugin_warning.rb +0 -14
- data/lib/ralph/output/nonzero_exit_warning.rb +0 -11
- data/lib/ralph/output/plugin_error.rb +0 -12
- data/lib/ralph/output/status.rb +0 -176
- data/lib/ralph/output/struggle_warning.rb +0 -18
- data/lib/ralph/output/task_completion.rb +0 -12
- data/lib/ralph/output/tasks_file_created.rb +0 -11
- data/lib/ralph/prompt_template.rb +0 -183
- data/lib/ralph/storage/context.rb +0 -58
- data/lib/ralph/storage/history.rb +0 -117
- data/lib/ralph/storage/state.rb +0 -178
- data/lib/ralph/storage/tasks.rb +0 -244
- data/lib/ralph/threads/heartbeat.rb +0 -44
- data/lib/ralph/threads/stream_reader.rb +0 -50
- data/original/bin/ralph.js +0 -13
- data/original/ralph.ts +0 -1706
- data/specs/iteration.md +0 -173
- data/specs/output.md +0 -104
- data/specs/storage/local-data-structure.md +0 -246
- data/specs/tasks.md +0 -295
data/lib/ralph/iteration.rb
DELETED
|
@@ -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
|
data/lib/ralph/output/banner.rb
DELETED
|
@@ -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,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
|
data/lib/ralph/output/status.rb
DELETED
|
@@ -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
|