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
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ralph
|
|
4
|
-
class PromptTemplate
|
|
5
|
-
class Error < StandardError; end
|
|
6
|
-
|
|
7
|
-
attr_reader :user_prompt, :context, :tasks
|
|
8
|
-
|
|
9
|
-
def initialize(user_prompt, context: nil, tasks: nil)
|
|
10
|
-
@user_prompt = user_prompt
|
|
11
|
-
@context = context
|
|
12
|
-
@tasks = tasks
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def to_s = @user_prompt
|
|
16
|
-
def empty? = @user_prompt.strip.empty?
|
|
17
|
-
|
|
18
|
-
def context_section
|
|
19
|
-
@_context_section ||=
|
|
20
|
-
if @context.present?
|
|
21
|
-
"
|
|
22
|
-
## Additional Context (added by user mid-loop)
|
|
23
|
-
|
|
24
|
-
#{@context.content}
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
"
|
|
28
|
-
else
|
|
29
|
-
""
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def task_mode_prompt(state)
|
|
34
|
-
tasks_section = build_tasks_section(state)
|
|
35
|
-
<<~PROMPT.strip
|
|
36
|
-
# Ralph Wiggum Loop - Iteration #{state.iteration}
|
|
37
|
-
|
|
38
|
-
You are in an iterative development loop working through a task list.
|
|
39
|
-
#{context_section}#{tasks_section}
|
|
40
|
-
## Your Main Goal
|
|
41
|
-
|
|
42
|
-
#{@user_prompt}
|
|
43
|
-
|
|
44
|
-
## Critical Rules
|
|
45
|
-
|
|
46
|
-
- Work on ONE task at a time from .ralph/ralph-tasks.md
|
|
47
|
-
- ONLY output <promise>#{state.task_promise}</promise> when the current task is complete and marked in ralph-tasks.md
|
|
48
|
-
- ONLY output <promise>#{state.completion_promise}</promise> when ALL tasks are truly done
|
|
49
|
-
- Do NOT lie or output false promises to exit the loop
|
|
50
|
-
- If stuck, try a different approach
|
|
51
|
-
- Check your work before claiming completion
|
|
52
|
-
|
|
53
|
-
## Current Iteration: #{state.iteration}#{state.max_iterations > 0 ? " / #{state.max_iterations}" : " (unlimited)"} (min: #{state.min_iterations || 1})
|
|
54
|
-
|
|
55
|
-
Tasks Mode: ENABLED - Work on one task at a time from ralph-tasks.md
|
|
56
|
-
|
|
57
|
-
Now, work on the current task. Good luck!
|
|
58
|
-
PROMPT
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def non_task_mode_prompt(state)
|
|
62
|
-
<<~PROMPT.strip
|
|
63
|
-
# Ralph Wiggum Loop - Iteration #{state.iteration}
|
|
64
|
-
|
|
65
|
-
You are in an iterative development loop. Work on the task below until you can genuinely complete it.
|
|
66
|
-
#{context_section}
|
|
67
|
-
## Your Task
|
|
68
|
-
|
|
69
|
-
#{@user_prompt}
|
|
70
|
-
|
|
71
|
-
## Instructions
|
|
72
|
-
|
|
73
|
-
1. Read the current state of files to understand what's been done
|
|
74
|
-
2. Track your progress and plan remaining work
|
|
75
|
-
3. Make progress on the task
|
|
76
|
-
4. Run tests/verification if applicable
|
|
77
|
-
5. When the task is GENUINELY COMPLETE, output:
|
|
78
|
-
<promise>#{state.completion_promise}</promise>
|
|
79
|
-
|
|
80
|
-
## Critical Rules
|
|
81
|
-
|
|
82
|
-
- ONLY output <promise>#{state.completion_promise}</promise> when the task is truly done
|
|
83
|
-
- Do NOT lie or output false promises to exit the loop
|
|
84
|
-
- If stuck, try a different approach
|
|
85
|
-
- Check your work before claiming completion
|
|
86
|
-
- The loop will continue until you succeed
|
|
87
|
-
|
|
88
|
-
## Current Iteration: #{state.iteration}#{state.max_iterations > 0 ? " / #{state.max_iterations}" : " (unlimited)"} (min: #{state.min_iterations || 1})
|
|
89
|
-
|
|
90
|
-
Now, work on the task. Good luck!
|
|
91
|
-
PROMPT
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Build the full iteration prompt sent to the LLM.
|
|
95
|
-
# +state+ is a Storage::State with iteration metadata.
|
|
96
|
-
# +_agent+ is the agent config (reserved for future use).
|
|
97
|
-
def build_iteration(state, _agent)
|
|
98
|
-
if state.tasks_mode
|
|
99
|
-
task_mode_prompt(state)
|
|
100
|
-
else
|
|
101
|
-
non_task_mode_prompt(state)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
private
|
|
106
|
-
|
|
107
|
-
def build_tasks_section(state)
|
|
108
|
-
tasks_path = @tasks.path
|
|
109
|
-
unless File.exist?(tasks_path)
|
|
110
|
-
return <<~SECTION
|
|
111
|
-
|
|
112
|
-
## TASKS MODE: Enabled (no tasks file found)
|
|
113
|
-
|
|
114
|
-
Create .ralph/ralph-tasks.md with your task list, or use `ralph --add-task "description"` to add tasks.
|
|
115
|
-
SECTION
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
begin
|
|
119
|
-
tasks_content = File.read(tasks_path)
|
|
120
|
-
tasks = Tasks.parse(tasks_content)
|
|
121
|
-
current_task = tasks.current
|
|
122
|
-
next_task = tasks.next
|
|
123
|
-
|
|
124
|
-
task_instructions =
|
|
125
|
-
if current_task
|
|
126
|
-
<<~INST
|
|
127
|
-
🔄 CURRENT TASK: "#{current_task.text}"
|
|
128
|
-
Focus on completing this specific task.
|
|
129
|
-
When done: Mark as [x] in .ralph/ralph-tasks.md and output <promise>#{state.task_promise}</promise>
|
|
130
|
-
INST
|
|
131
|
-
elsif next_task
|
|
132
|
-
<<~INST
|
|
133
|
-
📍 NEXT TASK: "#{next_task.text}"
|
|
134
|
-
Mark as [/] in .ralph/ralph-tasks.md before starting.
|
|
135
|
-
When done: Mark as [x] and output <promise>#{state.task_promise}</promise>
|
|
136
|
-
INST
|
|
137
|
-
elsif tasks.all_complete?
|
|
138
|
-
<<~INST
|
|
139
|
-
✅ ALL TASKS COMPLETE!
|
|
140
|
-
Output <promise>#{state.completion_promise}</promise> to finish.
|
|
141
|
-
INST
|
|
142
|
-
else
|
|
143
|
-
<<~INST
|
|
144
|
-
📋 No tasks found. Add tasks to .ralph/ralph-tasks.md or use `ralph --add-task`
|
|
145
|
-
INST
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
<<~SECTION
|
|
149
|
-
|
|
150
|
-
## TASKS MODE: Working through task list
|
|
151
|
-
|
|
152
|
-
Current tasks from .ralph/ralph-tasks.md:
|
|
153
|
-
```markdown
|
|
154
|
-
#{tasks_content.strip}
|
|
155
|
-
```
|
|
156
|
-
#{task_instructions}
|
|
157
|
-
### Task Workflow
|
|
158
|
-
1. Find any task marked [/] (in progress). If none, pick the first [ ] task.
|
|
159
|
-
2. Mark the task as [/] in ralph-tasks.md before starting.
|
|
160
|
-
3. Complete the task.
|
|
161
|
-
4. Mark as [x] when verified complete.
|
|
162
|
-
5. Output <promise>#{state.task_promise}</promise> to move to the next task.
|
|
163
|
-
6. Only output <promise>#{state.completion_promise}</promise> when ALL tasks are [x].
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
SECTION
|
|
167
|
-
rescue StandardError
|
|
168
|
-
<<~SECTION
|
|
169
|
-
|
|
170
|
-
## TASKS MODE: Error reading tasks file
|
|
171
|
-
|
|
172
|
-
Unable to read .ralph/ralph-tasks.md
|
|
173
|
-
SECTION
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
class << self
|
|
178
|
-
def inject(user_prompt, context:, tasks:)
|
|
179
|
-
new(user_prompt, context: context, tasks: tasks)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Ralph
|
|
6
|
-
module Storage
|
|
7
|
-
# Represents the persistent AI context file for conversational continuity.
|
|
8
|
-
#
|
|
9
|
-
# Always instantiate with Context.new — it represents the file on disk.
|
|
10
|
-
# Content is read lazily; the file is created on first write/append.
|
|
11
|
-
class Context
|
|
12
|
-
attr_reader :path
|
|
13
|
-
|
|
14
|
-
def initialize
|
|
15
|
-
FileUtils.mkdir_p(dir)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.dir
|
|
19
|
-
@dir ||= File.join(Dir.pwd, ".ralph")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def dir = self.class.dir
|
|
23
|
-
|
|
24
|
-
def path
|
|
25
|
-
@path ||= File.join(dir, "ralph-context.md")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def content
|
|
29
|
-
if File.exist?(path)
|
|
30
|
-
text = File.read(path).strip
|
|
31
|
-
text.empty? ? nil : text
|
|
32
|
-
end
|
|
33
|
-
rescue StandardError
|
|
34
|
-
nil
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def present? = !content.nil?
|
|
38
|
-
|
|
39
|
-
def append(text)
|
|
40
|
-
if File.exist?(path)
|
|
41
|
-
write(File.read(path) + text)
|
|
42
|
-
else
|
|
43
|
-
write("# Ralph Loop Context\n#{text}")
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def write(text)
|
|
48
|
-
File.write(path, text)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def clear
|
|
52
|
-
if File.exist?(path)
|
|
53
|
-
File.delete(path)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
|
|
6
|
-
module Ralph
|
|
7
|
-
module Storage
|
|
8
|
-
# Manages iteration history and performance tracking.
|
|
9
|
-
#
|
|
10
|
-
# Stores everything as plain hashes. No structs, no ceremony.
|
|
11
|
-
# Each iteration is a hash appended to an array, persisted as JSON.
|
|
12
|
-
class History
|
|
13
|
-
include ::Ralph::Helpers
|
|
14
|
-
|
|
15
|
-
EMPTY_HISTORY = {
|
|
16
|
-
"iterations" => [],
|
|
17
|
-
"total_duration_ms" => 0,
|
|
18
|
-
"struggle_indicators" => {
|
|
19
|
-
"repeated_errors" => {},
|
|
20
|
-
"no_progress_iterations" => 0,
|
|
21
|
-
"short_iterations" => 0
|
|
22
|
-
}
|
|
23
|
-
}.freeze
|
|
24
|
-
|
|
25
|
-
attr_reader :history
|
|
26
|
-
|
|
27
|
-
def initialize
|
|
28
|
-
@history = self.class.empty_history
|
|
29
|
-
self.class.save_history(@history)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Record a completed iteration.
|
|
33
|
-
def record(state_iteration:, iteration_start:, result:, struggle_indicators:)
|
|
34
|
-
entry = {
|
|
35
|
-
"iteration" => state_iteration,
|
|
36
|
-
"started_at" => Time.at(iteration_start / 1000.0).utc.iso8601,
|
|
37
|
-
"ended_at" => Time.now.utc.iso8601,
|
|
38
|
-
"duration_ms" => result.duration_ms,
|
|
39
|
-
"tools_used" => result.tool_counts,
|
|
40
|
-
"files_modified" => result.files_modified,
|
|
41
|
-
"exit_code" => result.exit_code,
|
|
42
|
-
"completion_detected" => result.completion_detected,
|
|
43
|
-
"errors" => result.errors
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
append(entry, result.duration_ms, struggle_indicators)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Record an iteration that raised an exception before producing a result.
|
|
50
|
-
def record_error(state_iteration:, iteration_start:, error:)
|
|
51
|
-
iteration_duration = now_ms - iteration_start
|
|
52
|
-
|
|
53
|
-
entry = {
|
|
54
|
-
"iteration" => state_iteration,
|
|
55
|
-
"started_at" => Time.at(iteration_start / 1000.0).utc.iso8601,
|
|
56
|
-
"ended_at" => Time.now.utc.iso8601,
|
|
57
|
-
"duration_ms" => iteration_duration,
|
|
58
|
-
"tools_used" => {},
|
|
59
|
-
"files_modified" => [],
|
|
60
|
-
"exit_code" => -1,
|
|
61
|
-
"completion_detected" => false,
|
|
62
|
-
"errors" => [error.to_s[0, 200]]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
append(entry, iteration_duration, nil)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def total_duration_ms
|
|
69
|
-
@history["total_duration_ms"]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# --- Class API (raw persistence) ---
|
|
73
|
-
|
|
74
|
-
class << self
|
|
75
|
-
def state_dir
|
|
76
|
-
File.join(Dir.pwd, ".ralph")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def history_path
|
|
80
|
-
File.join(state_dir, "ralph-history.json")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def empty_history
|
|
84
|
-
JSON.parse(JSON.generate(EMPTY_HISTORY))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def save_history(history)
|
|
88
|
-
FileUtils.mkdir_p(state_dir)
|
|
89
|
-
File.write(history_path, JSON.pretty_generate(history))
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def load_history
|
|
93
|
-
return empty_history unless File.exist?(history_path)
|
|
94
|
-
|
|
95
|
-
JSON.parse(File.read(history_path))
|
|
96
|
-
rescue StandardError
|
|
97
|
-
empty_history
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def clear_history
|
|
101
|
-
File.delete(history_path) if File.exist?(history_path)
|
|
102
|
-
rescue StandardError
|
|
103
|
-
# ignore
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
private
|
|
108
|
-
|
|
109
|
-
def append(entry, duration_ms, struggle_indicators)
|
|
110
|
-
@history["iterations"] << entry
|
|
111
|
-
@history["total_duration_ms"] += duration_ms
|
|
112
|
-
@history["struggle_indicators"] = struggle_indicators if struggle_indicators
|
|
113
|
-
self.class.save_history(@history)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
data/lib/ralph/storage/state.rb
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'fileutils'
|
|
5
|
-
|
|
6
|
-
module Ralph
|
|
7
|
-
module Storage
|
|
8
|
-
# Represents and persists Ralph loop state
|
|
9
|
-
class State
|
|
10
|
-
attr_accessor :active, :iteration
|
|
11
|
-
attr_reader :min_iterations, :max_iterations, :completion_promise,
|
|
12
|
-
:tasks_mode, :task_promise, :prompt, :started_at,
|
|
13
|
-
:model, :agent
|
|
14
|
-
|
|
15
|
-
def initialize(active:, iteration:, min_iterations:, max_iterations:,
|
|
16
|
-
completion_promise:, tasks_mode:, task_promise:, prompt:,
|
|
17
|
-
started_at:, model:, agent:)
|
|
18
|
-
@active = active
|
|
19
|
-
@iteration = iteration
|
|
20
|
-
@min_iterations = min_iterations
|
|
21
|
-
@max_iterations = max_iterations
|
|
22
|
-
@completion_promise = completion_promise
|
|
23
|
-
@tasks_mode = tasks_mode
|
|
24
|
-
@task_promise = task_promise
|
|
25
|
-
@prompt = prompt
|
|
26
|
-
@started_at = started_at
|
|
27
|
-
@model = model
|
|
28
|
-
@agent = agent
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def save
|
|
32
|
-
FileUtils.mkdir_p(self.class.dir)
|
|
33
|
-
File.write(self.class.path, JSON.pretty_generate(to_h))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def clear
|
|
37
|
-
File.delete(self.class.path) if File.exist?(self.class.path)
|
|
38
|
-
rescue StandardError
|
|
39
|
-
# ignore
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def to_h
|
|
43
|
-
{
|
|
44
|
-
active: @active,
|
|
45
|
-
iteration: @iteration,
|
|
46
|
-
minIterations: @min_iterations,
|
|
47
|
-
maxIterations: @max_iterations,
|
|
48
|
-
completionPromise: @completion_promise,
|
|
49
|
-
tasksMode: @tasks_mode,
|
|
50
|
-
taskPromise: @task_promise,
|
|
51
|
-
prompt: @prompt,
|
|
52
|
-
startedAt: @started_at,
|
|
53
|
-
model: @model,
|
|
54
|
-
agent: @agent
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
class << self
|
|
59
|
-
def from_config(config, prompt:)
|
|
60
|
-
new(
|
|
61
|
-
|
|
62
|
-
iteration: 1,
|
|
63
|
-
active: true,
|
|
64
|
-
prompt: prompt.to_s,
|
|
65
|
-
started_at: Time.now.utc.iso8601,
|
|
66
|
-
|
|
67
|
-
model: config.model,
|
|
68
|
-
agent: config.chosen_agent,
|
|
69
|
-
tasks_mode: config.tasks_mode,
|
|
70
|
-
task_promise: config.task_promise,
|
|
71
|
-
min_iterations: config.min_iterations,
|
|
72
|
-
max_iterations: config.max_iterations,
|
|
73
|
-
completion_promise: config.completion_promise,
|
|
74
|
-
)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def dir
|
|
78
|
-
File.join(Dir.pwd, '.ralph')
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def path
|
|
82
|
-
File.join(dir, 'ralph-loop.state.json')
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def load
|
|
86
|
-
if File.exist?(path)
|
|
87
|
-
JSON.parse(File.read(path)).then do |data|
|
|
88
|
-
new(
|
|
89
|
-
active: data['active'],
|
|
90
|
-
iteration: data['iteration'],
|
|
91
|
-
min_iterations: data['minIterations'],
|
|
92
|
-
max_iterations: data['maxIterations'],
|
|
93
|
-
completion_promise: data['completionPromise'],
|
|
94
|
-
tasks_mode: data['tasksMode'],
|
|
95
|
-
task_promise: data['taskPromise'],
|
|
96
|
-
prompt: data['prompt'],
|
|
97
|
-
started_at: data['startedAt'],
|
|
98
|
-
model: data['model'],
|
|
99
|
-
agent: data['agent']
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
rescue StandardError
|
|
104
|
-
nil
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def clear
|
|
108
|
-
File.delete(path) if File.exist?(path)
|
|
109
|
-
rescue StandardError
|
|
110
|
-
# ignore
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# --- Configuration Management ---
|
|
115
|
-
# NOTE: Commented out — this was generating a synthetic OpenCode config
|
|
116
|
-
# to inject via OPENCODE_CONFIG env var. It worked around OpenCode's
|
|
117
|
-
# permission system and reimplemented its config resolution logic.
|
|
118
|
-
# If needed again, this should be an OpenCode feature, not a workaround.
|
|
119
|
-
#
|
|
120
|
-
# def self.load_plugins_from_config(config_path)
|
|
121
|
-
# return [] unless File.exist?(config_path)
|
|
122
|
-
# raw = File.read(config_path)
|
|
123
|
-
# without_block = raw.gsub(/\/\*[\s\S]*?\*\//, "")
|
|
124
|
-
# without_line = without_block.gsub(/^\s*\/\/.*$/, "")
|
|
125
|
-
# parsed = JSON.parse(without_line)
|
|
126
|
-
# plugins = parsed["plugin"]
|
|
127
|
-
# return [] unless plugins.is_a?(Array)
|
|
128
|
-
# plugins.select { |p| p.is_a?(String) }
|
|
129
|
-
# rescue StandardError
|
|
130
|
-
# []
|
|
131
|
-
# end
|
|
132
|
-
#
|
|
133
|
-
# def self.ensure_ralph_config(filter_plugins: false, allow_all_permissions: false)
|
|
134
|
-
# FileUtils.mkdir_p(dir)
|
|
135
|
-
# config_path = File.join(dir, "ralph-opencode.config.json")
|
|
136
|
-
#
|
|
137
|
-
# xdg_config = ENV["XDG_CONFIG_HOME"] || File.join(ENV["HOME"] || "", ".config")
|
|
138
|
-
# user_config_path = File.join(xdg_config, "opencode", "opencode.json")
|
|
139
|
-
# project_config_path = File.join(Dir.pwd, ".ralph", "opencode.json")
|
|
140
|
-
# legacy_project_config_path = File.join(Dir.pwd, ".opencode", "opencode.json")
|
|
141
|
-
#
|
|
142
|
-
# config = { "$schema" => "https://opencode.ai/config.json" }
|
|
143
|
-
#
|
|
144
|
-
# if filter_plugins
|
|
145
|
-
# plugins = [
|
|
146
|
-
# *load_plugins_from_config(user_config_path),
|
|
147
|
-
# *load_plugins_from_config(project_config_path),
|
|
148
|
-
# *load_plugins_from_config(legacy_project_config_path)
|
|
149
|
-
# ].uniq.select { |p| p =~ /auth/i }
|
|
150
|
-
# config["plugin"] = plugins
|
|
151
|
-
# end
|
|
152
|
-
#
|
|
153
|
-
# if allow_all_permissions
|
|
154
|
-
# config["permission"] = {
|
|
155
|
-
# "read" => "allow",
|
|
156
|
-
# "edit" => "allow",
|
|
157
|
-
# "glob" => "allow",
|
|
158
|
-
# "grep" => "allow",
|
|
159
|
-
# "list" => "allow",
|
|
160
|
-
# "bash" => "allow",
|
|
161
|
-
# "task" => "allow",
|
|
162
|
-
# "webfetch" => "allow",
|
|
163
|
-
# "websearch" => "allow",
|
|
164
|
-
# "codesearch" => "allow",
|
|
165
|
-
# "todowrite" => "allow",
|
|
166
|
-
# "todoread" => "allow",
|
|
167
|
-
# "question" => "allow",
|
|
168
|
-
# "lsp" => "allow",
|
|
169
|
-
# "external_directory" => "allow"
|
|
170
|
-
# }
|
|
171
|
-
# end
|
|
172
|
-
#
|
|
173
|
-
# File.write(config_path, JSON.pretty_generate(config))
|
|
174
|
-
# config_path
|
|
175
|
-
# end
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
end
|