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/storage/tasks.rb
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Ralph
|
|
6
|
-
module Storage
|
|
7
|
-
# Manages task tracking and workflow coordination.
|
|
8
|
-
#
|
|
9
|
-
# Always instantiate with Tasks.new — it represents the tasks file on disk.
|
|
10
|
-
# Follows the same instance-based pattern as Context and History.
|
|
11
|
-
class Tasks
|
|
12
|
-
def initialize
|
|
13
|
-
FileUtils.mkdir_p(dir)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.dir
|
|
17
|
-
File.join(Dir.pwd, ".ralph")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def dir = self.class.dir
|
|
21
|
-
|
|
22
|
-
def self.path
|
|
23
|
-
File.join(dir, "ralph-tasks.md")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def path = self.class.path
|
|
27
|
-
|
|
28
|
-
# --- Tasks Management ---
|
|
29
|
-
def load_tasks
|
|
30
|
-
if File.exist?(path)
|
|
31
|
-
content = File.read(path)
|
|
32
|
-
TasksCollection.parse(content)
|
|
33
|
-
end
|
|
34
|
-
rescue StandardError
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def save_tasks(tasks)
|
|
39
|
-
content = "# Ralph Tasks\n\n"
|
|
40
|
-
tasks.each do |task|
|
|
41
|
-
content << task.to_s << "\n"
|
|
42
|
-
task.subtasks.each do |subtask|
|
|
43
|
-
content << " #{subtask}\n"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
File.write(path, content)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def clear_tasks
|
|
50
|
-
File.delete(path) if File.exist?(path)
|
|
51
|
-
rescue StandardError
|
|
52
|
-
# ignore
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def tasks_exist?
|
|
56
|
-
File.exist?(path)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# --- Single Task Operations ---
|
|
60
|
-
|
|
61
|
-
# Add a new task by description string.
|
|
62
|
-
# Creates the tasks file if it doesn't exist.
|
|
63
|
-
def add_task(description)
|
|
64
|
-
tasks = load_tasks || TasksCollection.new
|
|
65
|
-
task = Task.new(text: description, status: :todo)
|
|
66
|
-
tasks.add(task)
|
|
67
|
-
save_tasks(tasks)
|
|
68
|
-
task
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Remove a task by 1-based index.
|
|
72
|
-
# Returns the removed Task.
|
|
73
|
-
# Raises IndexError if index is out of range.
|
|
74
|
-
# Raises RuntimeError if no tasks file exists.
|
|
75
|
-
def remove_task(index)
|
|
76
|
-
raise "No tasks file found" unless tasks_exist?
|
|
77
|
-
|
|
78
|
-
tasks = load_tasks
|
|
79
|
-
removed = tasks.remove_at(index)
|
|
80
|
-
save_tasks(tasks)
|
|
81
|
-
removed
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# --- Task Initialization ---
|
|
85
|
-
def initialize_tasks_file
|
|
86
|
-
content = "# Ralph Tasks\n\nAdd your tasks below using: `ralph --add-task \"description\"`\n"
|
|
87
|
-
File.write(path, content)
|
|
88
|
-
path
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# --- Task Models ---
|
|
93
|
-
|
|
94
|
-
# Individual task with status and subtasks
|
|
95
|
-
class Task
|
|
96
|
-
attr_accessor :text, :status, :subtasks, :original_line
|
|
97
|
-
|
|
98
|
-
def initialize(text:, status: :todo, subtasks: [], original_line: nil)
|
|
99
|
-
@text = text
|
|
100
|
-
@status = status
|
|
101
|
-
@subtasks = subtasks
|
|
102
|
-
@original_line = original_line
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def status_char
|
|
106
|
-
case status
|
|
107
|
-
when :complete then "x"
|
|
108
|
-
when :in_progress then "/"
|
|
109
|
-
else " "
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def to_s
|
|
114
|
-
"- [#{status_char}] #{text}"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def todo? = status == :todo
|
|
118
|
-
def in_progress? = status == :in_progress
|
|
119
|
-
def complete? = status == :complete
|
|
120
|
-
|
|
121
|
-
def toggle_status
|
|
122
|
-
case status
|
|
123
|
-
when :todo then @status = :in_progress
|
|
124
|
-
when :in_progress then @status = :complete
|
|
125
|
-
when :complete then @status = :todo
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def mark_complete! = @status = :complete
|
|
130
|
-
def mark_in_progress! = @status = :in_progress
|
|
131
|
-
def mark_todo! = @status = :todo
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Collection of tasks with enumerable interface
|
|
135
|
-
class TasksCollection
|
|
136
|
-
include Enumerable
|
|
137
|
-
|
|
138
|
-
def initialize(tasks = [])
|
|
139
|
-
@tasks = tasks
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def each(&block) = @tasks.each(&block)
|
|
143
|
-
def count(&block) = @tasks.count(&block)
|
|
144
|
-
|
|
145
|
-
def empty? = @tasks.empty?
|
|
146
|
-
def length = @tasks.length
|
|
147
|
-
def any? = @tasks.any?
|
|
148
|
-
|
|
149
|
-
# Add a task to the collection
|
|
150
|
-
def add(task)
|
|
151
|
-
@tasks << task
|
|
152
|
-
self
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Remove a task (and its subtasks) by 1-based index.
|
|
156
|
-
# Returns the removed Task, or raises IndexError if out of range.
|
|
157
|
-
def remove_at(index)
|
|
158
|
-
if index < 1 || index > @tasks.length
|
|
159
|
-
raise IndexError, "Task index #{index} is out of range (1-#{@tasks.length})"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
@tasks.delete_at(index - 1)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def self.parse(content)
|
|
166
|
-
tasks = []
|
|
167
|
-
current_task = nil
|
|
168
|
-
|
|
169
|
-
content.each_line do |line|
|
|
170
|
-
# Top-level task: starts with "- [" at beginning (no leading whitespace)
|
|
171
|
-
if (match = line.match(/^- \[([ x\/])\]\s*(.+)/))
|
|
172
|
-
tasks << current_task if current_task
|
|
173
|
-
status_char = match[1]
|
|
174
|
-
text = match[2]
|
|
175
|
-
status = case status_char
|
|
176
|
-
when "x" then :complete
|
|
177
|
-
when "/" then :in_progress
|
|
178
|
-
else :todo
|
|
179
|
-
end
|
|
180
|
-
current_task = Task.new(text: text, status: status, subtasks: [], original_line: line.chomp)
|
|
181
|
-
next
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Subtask: starts with whitespace followed by "- ["
|
|
185
|
-
if (match = line.match(/^\s+- \[([ x\/])\]\s*(.+)/)) && current_task
|
|
186
|
-
status_char = match[1]
|
|
187
|
-
text = match[2]
|
|
188
|
-
status = case status_char
|
|
189
|
-
when "x" then :complete
|
|
190
|
-
when "/" then :in_progress
|
|
191
|
-
else :todo
|
|
192
|
-
end
|
|
193
|
-
current_task.subtasks << Task.new(text: text, status: status, subtasks: [], original_line: line.chomp)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
tasks << current_task if current_task
|
|
198
|
-
new(tasks)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Display tasks with numbering for CLI
|
|
202
|
-
def display_with_indices
|
|
203
|
-
if empty?
|
|
204
|
-
puts "No tasks found."
|
|
205
|
-
return
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
puts "Current tasks:"
|
|
209
|
-
each_with_index do |task, i|
|
|
210
|
-
icon = status_icon(task.status)
|
|
211
|
-
puts "#{i + 1}. #{icon} #{task.text}"
|
|
212
|
-
|
|
213
|
-
task.subtasks.each do |subtask|
|
|
214
|
-
sub_icon = status_icon(subtask.status)
|
|
215
|
-
puts " #{sub_icon} #{subtask.text}"
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def current = find { |t| t.status == :in_progress }
|
|
221
|
-
def next = find { |t| t.status == :todo }
|
|
222
|
-
|
|
223
|
-
def all_complete?
|
|
224
|
-
!empty? && all? { |t| t.status == :complete }
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def status_icon(status)
|
|
228
|
-
case status
|
|
229
|
-
when :complete then "✅"
|
|
230
|
-
when :in_progress then "🔄"
|
|
231
|
-
else "⏸️"
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def self.status_icon(status)
|
|
236
|
-
case status
|
|
237
|
-
when :complete then "✅"
|
|
238
|
-
when :in_progress then "🔄"
|
|
239
|
-
else "⏸️"
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Threads
|
|
5
|
-
class Heartbeat
|
|
6
|
-
include ::Ralph::Helpers
|
|
7
|
-
|
|
8
|
-
def initialize(iteration_start, heartbeat_interval_ms, timing, mutex)
|
|
9
|
-
@iteration_start = iteration_start
|
|
10
|
-
@heartbeat_interval_ms = heartbeat_interval_ms
|
|
11
|
-
@timing = timing
|
|
12
|
-
@mutex = mutex
|
|
13
|
-
|
|
14
|
-
@thread = Thread.new { run_loop }
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def stop
|
|
18
|
-
@thread&.kill
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def join
|
|
22
|
-
@thread&.join
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def run_loop
|
|
28
|
-
loop do
|
|
29
|
-
sleep(@heartbeat_interval_ms / 1000.0)
|
|
30
|
-
now_ms.then do |now|
|
|
31
|
-
if now - @mutex.synchronize { @timing[:last_printed_at] } >= @heartbeat_interval_ms
|
|
32
|
-
@mutex.synchronize do
|
|
33
|
-
puts "⏳ working... elapsed #{format_duration(now - @iteration_start)} · last activity #{format_duration(now - @timing[:last_activity_at])} ago"
|
|
34
|
-
@timing[:last_printed_at] = now_ms
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
rescue StandardError
|
|
40
|
-
# thread cleanup
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
module Threads
|
|
5
|
-
class StreamReader
|
|
6
|
-
def initialize(io, text_buffer, mutex, tool_counts, on_line, is_error, tool_parser)
|
|
7
|
-
@io = io
|
|
8
|
-
@text_buffer = text_buffer
|
|
9
|
-
@mutex = mutex
|
|
10
|
-
@tool_counts = tool_counts
|
|
11
|
-
@on_line = on_line
|
|
12
|
-
@is_error = is_error
|
|
13
|
-
@tool_parser = tool_parser
|
|
14
|
-
|
|
15
|
-
@thread = Thread.new { run_loop }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def stop
|
|
19
|
-
@thread&.kill
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def join
|
|
23
|
-
@thread&.join
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def run_loop
|
|
29
|
-
buffer = +""
|
|
30
|
-
while (chunk = @io.read(4096))
|
|
31
|
-
@text_buffer << chunk
|
|
32
|
-
buffer << chunk
|
|
33
|
-
while (index = buffer.index("\n"))
|
|
34
|
-
line = buffer.slice!(0, index + 1).chomp
|
|
35
|
-
tool = @tool_parser.call(line)
|
|
36
|
-
@mutex.synchronize { @tool_counts[tool] += 1 } if tool
|
|
37
|
-
@on_line.call(line, @is_error, tool) if @on_line
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
unless buffer.empty?
|
|
41
|
-
tool = @tool_parser.call(buffer)
|
|
42
|
-
@mutex.synchronize { @tool_counts[tool] += 1 } if tool
|
|
43
|
-
@on_line.call(buffer, @is_error, tool) if @on_line
|
|
44
|
-
end
|
|
45
|
-
rescue IOError
|
|
46
|
-
# stream closed
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
data/original/bin/ralph.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const { spawnSync } = require("node:child_process");
|
|
3
|
-
const { resolve } = require("node:path");
|
|
4
|
-
|
|
5
|
-
const scriptPath = resolve(__dirname, "..", "ralph.ts");
|
|
6
|
-
const result = spawnSync("bun", [scriptPath, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
7
|
-
|
|
8
|
-
if (result.error) {
|
|
9
|
-
console.error("Error: Bun is required to run ralph. Install Bun: https://bun.sh");
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
process.exit(result.status ?? 1);
|