ralph.rb 1.2.435535439 → 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/loop.rb
CHANGED
|
@@ -1,196 +1,139 @@
|
|
|
1
|
-
# _ _ _
|
|
2
|
-
# _ __ __ _| |_ __ | |__ __ _(_) __ _ __ _ _ _ _ __ ___
|
|
3
|
-
# | '__/ _` | | '_ \| '_ \ \ \ /\ / / |/ _` |/ _` | | | | '_ ` _ \
|
|
4
|
-
# | | | (_| | | |_) | | | | \ V V /| | (_| | (_| | |_| | | | | | |
|
|
5
|
-
# |_| \__,_|_| .__/|_| |_| \_/\_/ |_|\__, |\__, |\__,_|_| |_| |_|
|
|
6
|
-
# |_| |___/ |___/
|
|
7
|
-
#
|
|
8
1
|
# frozen_string_literal: true
|
|
9
2
|
|
|
10
3
|
module Ralph
|
|
4
|
+
# The core iteration engine. Runs opencode in a loop, restarting fresh
|
|
5
|
+
# iterations whenever context grows too large or time limits are hit.
|
|
6
|
+
# The loop ends when:
|
|
7
|
+
# - the agent emits the completion string
|
|
8
|
+
# - max iterations are reached
|
|
9
|
+
# - total duration is exceeded
|
|
11
10
|
class Loop
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@
|
|
31
|
-
@
|
|
32
|
-
@
|
|
11
|
+
SYSTEM_PROMPT = <<~PROMPT
|
|
12
|
+
You are working autonomously in a loop. IMPORTANT RULES:
|
|
13
|
+
1. Do NOT ask the user any questions. Do NOT wait for user input.
|
|
14
|
+
If you need information, read the specs, code, or docs yourself.
|
|
15
|
+
2. Work through the task methodically. Use the todo list to track progress.
|
|
16
|
+
3. When you have FULLY completed the task, you MUST output the exact
|
|
17
|
+
completion string on its own line: %<completion>s
|
|
18
|
+
4. Do not output the completion string until ALL work is truly done.
|
|
19
|
+
PROMPT
|
|
20
|
+
|
|
21
|
+
attr_reader :metrics, :iteration_number, :completed
|
|
22
|
+
|
|
23
|
+
def initialize(options)
|
|
24
|
+
@prompt = options[:prompt]
|
|
25
|
+
@model = options[:model]
|
|
26
|
+
@max_iterations = options[:max_iterations]
|
|
27
|
+
@duration_limit = options[:duration]
|
|
28
|
+
@max_context = options[:max_context]
|
|
29
|
+
@completion_string = options[:completion] || "<promise>COMPLETE</promise>"
|
|
30
|
+
@metrics = Metrics.new
|
|
31
|
+
@iteration_number = 0
|
|
32
|
+
@completed = false
|
|
33
|
+
@started_at = nil
|
|
34
|
+
@display = Display.new(self)
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
# Run the main loop until a termination condition is met.
|
|
37
38
|
def run
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
@started_at = now_seconds
|
|
40
|
+
@display.show_start(@prompt)
|
|
41
|
+
|
|
42
|
+
loop do
|
|
43
|
+
break if should_stop_loop?
|
|
44
|
+
|
|
45
|
+
@iteration_number += 1
|
|
46
|
+
@metrics.new_iteration
|
|
47
|
+
@display.show_iteration_start
|
|
48
|
+
|
|
49
|
+
run_iteration
|
|
50
|
+
|
|
51
|
+
@display.show_iteration_end
|
|
41
52
|
end
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# |--------------------------------------------------|
|
|
47
|
-
# |==================================================|
|
|
48
|
-
# |**************************************************|
|
|
49
|
-
# |##################################################|
|
|
50
|
-
# | Main loop |
|
|
51
|
-
# |##################################################|
|
|
52
|
-
# |**************************************************|
|
|
53
|
-
# |==================================================|
|
|
54
|
-
# |--------------------------------------------------|
|
|
55
|
-
# |..................................................|
|
|
56
|
-
#
|
|
57
|
-
# © 2026 Nathan K.
|
|
58
|
-
# Honestly, I've no idea where this graphic
|
|
59
|
-
# wonder came from. It's MIT lisenced now, thought.
|
|
60
|
-
|
|
61
|
-
setup_signal_handler
|
|
54
|
+
@display.show_summary
|
|
55
|
+
@completed
|
|
56
|
+
end
|
|
62
57
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
break
|
|
70
|
-
else
|
|
71
|
-
Output::Iteration::Header.call(self)
|
|
72
|
-
|
|
73
|
-
iteration = Iteration.new(self)
|
|
74
|
-
result = iteration.run
|
|
75
|
-
should_continue = process_result(result, iteration)
|
|
76
|
-
|
|
77
|
-
if should_continue
|
|
78
|
-
@state.iteration += 1
|
|
79
|
-
@state.save
|
|
80
|
-
sleep 1
|
|
81
|
-
else
|
|
82
|
-
break
|
|
83
|
-
end
|
|
84
|
-
end
|
|
58
|
+
# Total elapsed wall-clock seconds since the loop started
|
|
59
|
+
def elapsed_seconds
|
|
60
|
+
if @started_at
|
|
61
|
+
now_seconds - @started_at
|
|
62
|
+
else
|
|
63
|
+
0.0
|
|
85
64
|
end
|
|
86
65
|
end
|
|
87
66
|
|
|
88
67
|
private
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
def should_stop_loop?
|
|
70
|
+
if @completed
|
|
71
|
+
true
|
|
72
|
+
elsif @max_iterations && @iteration_number >= @max_iterations
|
|
73
|
+
@display.show_termination("max iterations reached (#{@max_iterations})")
|
|
74
|
+
true
|
|
75
|
+
elsif @duration_limit && elapsed_seconds >= @duration_limit
|
|
76
|
+
@display.show_termination("duration limit reached (#{@duration_limit}s)")
|
|
77
|
+
true
|
|
78
|
+
else
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_iteration
|
|
84
|
+
iteration_started_at = now_seconds
|
|
85
|
+
agent = Opencode.new(model: @model)
|
|
86
|
+
full_prompt = build_prompt
|
|
87
|
+
|
|
88
|
+
agent.run(full_prompt) do |event|
|
|
89
|
+
@metrics.process(event)
|
|
90
|
+
@display.show_event(event)
|
|
91
|
+
|
|
92
|
+
if event.is_a?(Events::Text)
|
|
93
|
+
check_completion(event.text)
|
|
93
94
|
end
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
iteration_start: iteration.iteration_start,
|
|
100
|
-
result: result,
|
|
101
|
-
struggle_indicators: iteration.struggle_indicators
|
|
102
|
-
)
|
|
103
|
-
Output::PluginError.call
|
|
104
|
-
@state.clear
|
|
105
|
-
exit 1
|
|
106
|
-
|
|
107
|
-
when :failed
|
|
108
|
-
@history.record(
|
|
109
|
-
state_iteration: @state.iteration,
|
|
110
|
-
iteration_start: iteration.iteration_start,
|
|
111
|
-
result: result,
|
|
112
|
-
struggle_indicators: iteration.struggle_indicators
|
|
113
|
-
)
|
|
114
|
-
Output::NonzeroExitWarning.call(self, result)
|
|
115
|
-
|
|
116
|
-
if @config.tasks_mode
|
|
117
|
-
if check_completion(result.combined_output, @config.task_promise)
|
|
118
|
-
Output::TaskCompletion.call(self)
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
when :completed
|
|
123
|
-
@history.record(
|
|
124
|
-
state_iteration: @state.iteration,
|
|
125
|
-
iteration_start: iteration.iteration_start,
|
|
126
|
-
result: result,
|
|
127
|
-
struggle_indicators: iteration.struggle_indicators
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
if @state.iteration >= @config.min_iterations
|
|
131
|
-
Output::CompletionDetected.call(self)
|
|
132
|
-
@state.clear
|
|
133
|
-
Storage::History.clear_history
|
|
134
|
-
@context.clear
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
when :continuing
|
|
138
|
-
@history.record(
|
|
139
|
-
state_iteration: @state.iteration,
|
|
140
|
-
iteration_start: iteration.iteration_start,
|
|
141
|
-
result: result,
|
|
142
|
-
struggle_indicators: iteration.struggle_indicators
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if @state.iteration > 2 && iteration.struggling?
|
|
146
|
-
Output::StruggleWarning.call(self)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
if @config.tasks_mode
|
|
150
|
-
if check_completion(result.combined_output, @config.task_promise)
|
|
151
|
-
Output::TaskCompletion.call(self)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
if iteration.context_at_start.present?
|
|
156
|
-
Output::ContextConsumed.call
|
|
157
|
-
iteration.context_at_start.clear
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
when :error
|
|
161
|
-
@history.record_error(
|
|
162
|
-
state_iteration: @state.iteration,
|
|
163
|
-
iteration_start: iteration.iteration_start,
|
|
164
|
-
error: result.errors.first || "Unknown error"
|
|
165
|
-
)
|
|
96
|
+
if should_cancel_iteration?(iteration_started_at)
|
|
97
|
+
@display.show_iteration_cancelled(cancel_reason(iteration_started_at))
|
|
98
|
+
agent.cancel
|
|
99
|
+
break
|
|
166
100
|
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
167
103
|
|
|
168
|
-
|
|
104
|
+
def should_cancel_iteration?(iteration_started_at)
|
|
105
|
+
if @max_context && @metrics.current_context >= @max_context
|
|
106
|
+
true
|
|
107
|
+
elsif @duration_limit && elapsed_seconds >= @duration_limit
|
|
108
|
+
true
|
|
109
|
+
else
|
|
110
|
+
false
|
|
169
111
|
end
|
|
112
|
+
end
|
|
170
113
|
|
|
171
|
-
|
|
172
|
-
|
|
114
|
+
def cancel_reason(iteration_started_at)
|
|
115
|
+
if @max_context && @metrics.current_context >= @max_context
|
|
116
|
+
"context limit reached (#{@metrics.current_context}/#{@max_context})"
|
|
117
|
+
elsif @duration_limit && elapsed_seconds >= @duration_limit
|
|
118
|
+
"duration limit reached"
|
|
119
|
+
else
|
|
120
|
+
"unknown"
|
|
173
121
|
end
|
|
122
|
+
end
|
|
174
123
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
warn "\nForce stopping..."
|
|
179
|
-
exit 1
|
|
180
|
-
end
|
|
181
|
-
@config.stopping = true
|
|
182
|
-
warn "\nGracefully stopping Ralph loop..."
|
|
183
|
-
if @config.current_pid
|
|
184
|
-
begin
|
|
185
|
-
Process.kill('TERM', @config.current_pid)
|
|
186
|
-
rescue StandardError
|
|
187
|
-
# process may have exited
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
@state.clear
|
|
191
|
-
warn 'Loop cancelled.'
|
|
192
|
-
exit 0
|
|
193
|
-
end
|
|
124
|
+
def check_completion(text)
|
|
125
|
+
if text && text.include?(@completion_string)
|
|
126
|
+
@completed = true
|
|
194
127
|
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_prompt
|
|
131
|
+
system_instructions = format(SYSTEM_PROMPT, completion: @completion_string)
|
|
132
|
+
"#{system_instructions}\n\n---\n\n#{@prompt}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def now_seconds
|
|
136
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
137
|
+
end
|
|
195
138
|
end
|
|
196
139
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
# Tracks token usage and context size from opencode JSON stream events.
|
|
5
|
+
#
|
|
6
|
+
# Context formula per step: input + cache.read + cache.write
|
|
7
|
+
# Each step's cache.read ~= previous step's (cache.read + cache.write).
|
|
8
|
+
class Metrics
|
|
9
|
+
attr_reader :steps, :iteration_steps, :total_input_tokens, :total_output_tokens
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@steps = []
|
|
13
|
+
@iteration_steps = []
|
|
14
|
+
@total_input_tokens = 0
|
|
15
|
+
@total_output_tokens = 0
|
|
16
|
+
@iteration_count = 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Process a parsed event. Only StepFinish events carry token data.
|
|
20
|
+
def process(event)
|
|
21
|
+
if event.is_a?(Events::StepFinish)
|
|
22
|
+
record = {
|
|
23
|
+
input: event.input_tokens,
|
|
24
|
+
output: event.output_tokens,
|
|
25
|
+
reasoning: event.reasoning_tokens,
|
|
26
|
+
cache_read: event.cache_read,
|
|
27
|
+
cache_write: event.cache_write,
|
|
28
|
+
context: event.context_size,
|
|
29
|
+
timestamp: event.timestamp
|
|
30
|
+
}
|
|
31
|
+
@steps << record
|
|
32
|
+
@iteration_steps << record
|
|
33
|
+
@total_input_tokens += event.input_tokens
|
|
34
|
+
@total_output_tokens += event.output_tokens
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Current context size from the most recent step_finish
|
|
39
|
+
def current_context
|
|
40
|
+
if @steps.any?
|
|
41
|
+
@steps.last[:context]
|
|
42
|
+
else
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Total tokens consumed across all steps (input + output)
|
|
48
|
+
def tokens_consumed
|
|
49
|
+
@steps.sum { |step| step[:input] + step[:output] + step[:cache_read] + step[:cache_write] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Tokens consumed in the current iteration only
|
|
53
|
+
def iteration_tokens
|
|
54
|
+
@iteration_steps.sum { |step| step[:input] + step[:output] + step[:cache_read] + step[:cache_write] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Number of LLM steps completed
|
|
58
|
+
def step_count
|
|
59
|
+
@steps.length
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Signal a new iteration -- resets per-iteration tracking
|
|
63
|
+
def new_iteration
|
|
64
|
+
@iteration_count += 1
|
|
65
|
+
@iteration_steps = []
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Context growth rate: average tokens added per step
|
|
69
|
+
def context_growth_rate
|
|
70
|
+
if @steps.length >= 2
|
|
71
|
+
first_context = @steps.first[:context]
|
|
72
|
+
last_context = @steps.last[:context]
|
|
73
|
+
(last_context - first_context).to_f / (@steps.length - 1)
|
|
74
|
+
else
|
|
75
|
+
0.0
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Reset all metrics (used for full restart)
|
|
80
|
+
def reset
|
|
81
|
+
@steps = []
|
|
82
|
+
@iteration_steps = []
|
|
83
|
+
@total_input_tokens = 0
|
|
84
|
+
@total_output_tokens = 0
|
|
85
|
+
@iteration_count = 0
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
# Wraps the opencode CLI to run headless agent sessions with JSON stream output.
|
|
5
|
+
#
|
|
6
|
+
# Spawns `opencode run` as a subprocess and yields parsed events line-by-line
|
|
7
|
+
# from the JSON stream. Supports cancellation via Process.kill.
|
|
8
|
+
class Opencode
|
|
9
|
+
attr_reader :model, :agent, :pid
|
|
10
|
+
|
|
11
|
+
def initialize(model: nil, agent: nil)
|
|
12
|
+
@model = model
|
|
13
|
+
@agent = agent
|
|
14
|
+
@pid = nil
|
|
15
|
+
@process_thread = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Run opencode with the given prompt. Yields each parsed Event to the block.
|
|
19
|
+
# Returns the exit status of the opencode process.
|
|
20
|
+
def run(prompt, &block)
|
|
21
|
+
command = build_command(prompt)
|
|
22
|
+
@pid = nil
|
|
23
|
+
|
|
24
|
+
Open3.popen3(*command) do |stdin, stdout, stderr, wait_thread|
|
|
25
|
+
@pid = wait_thread.pid
|
|
26
|
+
stdin.close
|
|
27
|
+
|
|
28
|
+
stdout.each_line do |line|
|
|
29
|
+
line.strip!
|
|
30
|
+
if line.length > 0
|
|
31
|
+
Events.parse(line).then do |event|
|
|
32
|
+
block.call(event) if event
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
wait_thread.value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Send SIGTERM to the running opencode process.
|
|
42
|
+
def cancel
|
|
43
|
+
if @pid
|
|
44
|
+
Process.kill("TERM", @pid)
|
|
45
|
+
end
|
|
46
|
+
rescue Errno::ESRCH
|
|
47
|
+
# Process already exited -- nothing to do
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check whether opencode is available on the system PATH.
|
|
51
|
+
def self.available?
|
|
52
|
+
system("which opencode > /dev/null 2>&1")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_command(prompt)
|
|
58
|
+
command = ["opencode", "run"]
|
|
59
|
+
command.push("--model", @model) if @model
|
|
60
|
+
command.push("--agent", @agent) if @agent
|
|
61
|
+
command.push("--format", "json")
|
|
62
|
+
command.push(prompt)
|
|
63
|
+
command
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/ralph/version.rb
CHANGED
data/lib/ralph.rb
CHANGED
|
@@ -60,8 +60,5 @@ module Ralph
|
|
|
60
60
|
# ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣤⣄⣀⣀⣈⣉⠉⠉⠉⠉⠙⠛⠛⠛⠛⠛⠉⠉⣀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Helpers must load first since other modules extend it at parse time
|
|
64
|
-
require_relative "ralph/helpers"
|
|
65
|
-
|
|
66
63
|
# Require everything else by globbing, because I'm too lazy to do anything else
|
|
67
64
|
Dir[File.join(__dir__, "ralph", "**", "*.rb")].sort.each { |f| require f }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Ralph.rb Complete Implementation Plan
|
|
2
|
+
|
|
3
|
+
This is the master plan that coordinates all individual component implementations into a cohesive system.
|
|
4
|
+
|
|
5
|
+
## Phase 1: Foundation Setup
|
|
6
|
+
- [ ] Verify Ruby development environment and dependencies
|
|
7
|
+
- [ ] Set up project structure following Ruby conventions
|
|
8
|
+
- [ ] Create basic gemspec and package configuration
|
|
9
|
+
- [ ] Set up testing framework (RSpec or Minitest)
|
|
10
|
+
- [ ] Configure RuboCop style checking
|
|
11
|
+
- [ ] Create basic CI/CD configuration files
|
|
12
|
+
|
|
13
|
+
## Phase 2: Core Infrastructure (Parallel Development)
|
|
14
|
+
- [ ] **CLI Implementation** (Plan 01)
|
|
15
|
+
- [ ] Basic executable and argument parsing
|
|
16
|
+
- [ ] Option validation and error handling
|
|
17
|
+
- [ ] Unix pipe support implementation
|
|
18
|
+
- [ ] **Metrics Foundation** (Plan 04)
|
|
19
|
+
- [ ] Event parsing infrastructure
|
|
20
|
+
- [ ] JSON stream processing
|
|
21
|
+
- [ ] Basic token calculation logic
|
|
22
|
+
- [ ] **Agent Integration Foundation** (Plan 03)
|
|
23
|
+
- [ ] Opencode wrapper class
|
|
24
|
+
- [ ] Process management basics
|
|
25
|
+
- [ ] JSON stream consumption
|
|
26
|
+
|
|
27
|
+
## Phase 3: Loop Integration Core
|
|
28
|
+
- [ ] **Loop Implementation** (Plan 02)
|
|
29
|
+
- [ ] Basic loop architecture
|
|
30
|
+
- [ ] Iteration management system
|
|
31
|
+
- [ ] Context guard implementation
|
|
32
|
+
- [ ] **Component Integration**
|
|
33
|
+
- [ ] Metrics → Loop interface for context monitoring
|
|
34
|
+
- [ ] Loop → Agents interface for iteration control
|
|
35
|
+
- [ ] CLI → Loop interface for parameter passing
|
|
36
|
+
|
|
37
|
+
## Phase 4: Advanced Features Integration
|
|
38
|
+
- [ ] **Complete Agent Integration**
|
|
39
|
+
- [ ] Full opencode command execution
|
|
40
|
+
- [ ] Event forwarding to Metrics
|
|
41
|
+
- [ ] Process lifecycle management
|
|
42
|
+
- [ ] **Advanced Metrics**
|
|
43
|
+
- [ ] Real-time monitoring interface
|
|
44
|
+
- [ ] Threshold-based alerting
|
|
45
|
+
- [ ] Historical data tracking
|
|
46
|
+
- [ ] **Loop Termination Logic**
|
|
47
|
+
- [ ] Completion string detection
|
|
48
|
+
- [ ] Time and iteration limits
|
|
49
|
+
- [ ] Context-based cancellation
|
|
50
|
+
|
|
51
|
+
## Phase 5: Display and User Experience
|
|
52
|
+
- [ ] Real-time progress display implementation
|
|
53
|
+
- [ ] Token usage visualization
|
|
54
|
+
- [ ] Error message formatting and display
|
|
55
|
+
- [ ] Help system and documentation
|
|
56
|
+
- [ ] Debug and verbose output modes
|
|
57
|
+
|
|
58
|
+
## Phase 6: Testing and Quality Assurance
|
|
59
|
+
- [ ] **Unit Testing**
|
|
60
|
+
- [ ] CLI argument parsing tests
|
|
61
|
+
- [ ] Metrics calculation tests
|
|
62
|
+
- [ ] Loop logic tests
|
|
63
|
+
- [ ] Agent integration tests
|
|
64
|
+
- [ ] **Integration Testing**
|
|
65
|
+
- [ ] End-to-end workflow tests
|
|
66
|
+
- [ ] Error scenario testing
|
|
67
|
+
- [ ] Performance testing
|
|
68
|
+
- [ ] **Style and Quality**
|
|
69
|
+
- [ ] RuboCop compliance verification
|
|
70
|
+
- [ ] Code coverage requirements
|
|
71
|
+
- [ ] Documentation completeness
|
|
72
|
+
|
|
73
|
+
## Phase 7: Documentation and Deployment
|
|
74
|
+
- [ ] Complete README with usage examples
|
|
75
|
+
- [ ] API documentation for all components
|
|
76
|
+
- [ ] Installation and setup guides
|
|
77
|
+
- [ ] Troubleshooting and FAQ sections
|
|
78
|
+
- [ ] Release notes and changelog
|
|
79
|
+
|
|
80
|
+
## Phase 8: Performance Optimization
|
|
81
|
+
- [ ] Profile and optimize hot paths
|
|
82
|
+
- [ ] Memory usage optimization
|
|
83
|
+
- [ ] JSON stream parsing performance
|
|
84
|
+
- [ ] Concurrent processing optimizations
|
|
85
|
+
- [ ] Resource cleanup and garbage collection
|
|
86
|
+
|
|
87
|
+
## Critical Success Criteria
|
|
88
|
+
- [ ] All four component plans completed successfully
|
|
89
|
+
- [ ] Integration between components works seamlessly
|
|
90
|
+
- [ ] Real-time monitoring meets performance requirements
|
|
91
|
+
- [ ] Unix philosophy and CLI design principles followed
|
|
92
|
+
- [ ] Error handling is comprehensive and user-friendly
|
|
93
|
+
- [ ] Code quality meets Ruby style standards
|
|
94
|
+
- [ ] Test coverage exceeds 90%
|
|
95
|
+
- [ ] Documentation is complete and accurate
|
|
96
|
+
|
|
97
|
+
## Risk Mitigation
|
|
98
|
+
- [ ] Component integration points identified and tested early
|
|
99
|
+
- [ ] Fallback mechanisms for opencode CLI failures
|
|
100
|
+
- [ ] Graceful degradation for partial system failures
|
|
101
|
+
- [ ] Comprehensive logging for troubleshooting
|
|
102
|
+
- [ ] Modular design allows component replacement/upgrades
|
|
103
|
+
|
|
104
|
+
## Dependencies and External Requirements
|
|
105
|
+
- [ ] Opencode CLI installed and accessible
|
|
106
|
+
- [ ] Ruby 3.0+ with standard library
|
|
107
|
+
- [ ] Unix-like environment for pipe support
|
|
108
|
+
- [ ] Sufficient memory for JSON stream processing
|
|
109
|
+
- [ ] Network access for opencode API communication
|
|
110
|
+
|
|
111
|
+
## Final Verification Checklist
|
|
112
|
+
- [ ] CLI accepts all specified options correctly
|
|
113
|
+
- [ ] Loop manages iterations and context properly
|
|
114
|
+
- [ ] Agents communicate with opencode reliably
|
|
115
|
+
- [ ] Metrics provide accurate real-time data
|
|
116
|
+
- [ ] Complete workflow functions end-to-end
|
|
117
|
+
- [ ] All error scenarios handled gracefully
|
|
118
|
+
- [ ] Performance meets real-time requirements
|
|
119
|
+
- [ ] Documentation enables easy adoption
|
|
120
|
+
- [ ] Code quality and tests pass all checks
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Ralph.rb CLI Implementation Plan
|
|
2
|
+
|
|
3
|
+
This plan outlines the phased implementation of the Ralph.rb command-line interface based on the CLI specification in `specs/cli.md`.
|
|
4
|
+
|
|
5
|
+
## Phase 1: Core CLI Infrastructure
|
|
6
|
+
- [ ] Create `exe/ralph` executable with proper shebang and permissions
|
|
7
|
+
- [ ] Set up basic argument parsing using Ruby's OptionParser or similar
|
|
8
|
+
- [ ] Implement standard Unix-style pipe support for stdin input
|
|
9
|
+
- [ ] Add basic help/version output functionality
|
|
10
|
+
- [ ] Set up project structure with lib/ directory and main.rb entry point
|
|
11
|
+
|
|
12
|
+
## Phase 2: Command Line Options Implementation
|
|
13
|
+
- [ ] Implement `--model` option for model selection validation
|
|
14
|
+
- [ ] Add `--max-iterations` parameter with integer validation
|
|
15
|
+
- [ ] Implement `--duration` option with time parsing (seconds/minutes)
|
|
16
|
+
- [ ] Add `--max-context` option with integer validation
|
|
17
|
+
- [ ] Implement `--completion` option for completion string detection
|
|
18
|
+
- [ ] Add validation for required vs optional parameters
|
|
19
|
+
|
|
20
|
+
## Phase 3: Input Processing and Validation
|
|
21
|
+
- [ ] Implement stdin reading with proper encoding handling
|
|
22
|
+
- [ ] Create input validation for prompt files vs inline prompts
|
|
23
|
+
- [ ] Add error handling for malformed input or missing files
|
|
24
|
+
- [ ] Implement argument combination logic (stdin + args)
|
|
25
|
+
- [ ] Add debug mode for troubleshooting input processing
|
|
26
|
+
|
|
27
|
+
## Phase 4: Integration Preparation
|
|
28
|
+
- [ ] Create interface stubs for Loop, Agent, and Metrics components
|
|
29
|
+
- [ ] Implement parameter passing to core loop system
|
|
30
|
+
- [ ] Add configuration management and defaults
|
|
31
|
+
- [ ] Create error handling and user-friendly error messages
|
|
32
|
+
- [ ] Implement logging/verbosity levels if needed
|
|
33
|
+
|
|
34
|
+
## Phase 5: Testing and Validation
|
|
35
|
+
- [ ] Write unit tests for CLI argument parsing
|
|
36
|
+
- [ ] Test pipe functionality with various input sources
|
|
37
|
+
- [ ] Validate error handling for invalid inputs
|
|
38
|
+
- [ ] Test integration with other components (mock versions)
|
|
39
|
+
- [ ] Add integration tests for complete CLI workflow
|
|
40
|
+
|
|
41
|
+
## Verification Criteria
|
|
42
|
+
- [ ] CLI accepts all specified options with proper validation
|
|
43
|
+
- [ ] Unix pipe functionality works correctly (`cat file | ralph`)
|
|
44
|
+
- [ ] Error messages are clear and helpful
|
|
45
|
+
- [ ] All options pass parameters correctly to core system
|
|
46
|
+
- [ ] Help documentation is comprehensive and accurate
|
|
47
|
+
- [ ] No Ruby style violations (run `bin/rubocop`)
|
|
48
|
+
- [ ] All tests pass (run `bin/test`)
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
- Requires Loop, Agents, and Metrics components to be implemented
|
|
52
|
+
- Needs Ruby OptionParser or equivalent for argument handling
|
|
53
|
+
- Integration with opencil CLI for agent execution
|