ralph.rb 2.0.0 → 2.1.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/Gemfile.lock +1 -1
- data/README.md +3 -8
- data/lib/ralph/cli.rb +95 -58
- data/lib/ralph/display.rb +5 -0
- data/lib/ralph/iteration.rb +100 -0
- data/lib/ralph/loop.rb +61 -63
- data/lib/ralph/prompt/build.rb +60 -0
- data/lib/ralph/prompt/plan.rb +49 -0
- data/lib/ralph/version.rb +1 -1
- data/plans/00-complete-implementation.md +10 -2
- data/plans/01-cli-implementation.md +34 -22
- data/plans/02-loop-implementation.md +54 -54
- data/plans/05-prompts-implementation.md +66 -0
- data/plans/README.md +8 -3
- data/ralph.jpg +0 -0
- data/reading/ralph-playbook.md +2119 -0
- data/specs/README.md +5 -4
- data/specs/cli.md +32 -4
- data/specs/loop.md +47 -13
- data/specs/prompts.md +137 -0
- metadata +9 -10
- 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 +0 -32
- data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
- data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
- data/specs/lib/todo_item.rb +0 -144
- data/specs/log +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5554c13454fc43d04f41b7f969653cb7d51243eb40df245aefad31d82b38ab3a
|
|
4
|
+
data.tar.gz: 97909bf34b37d86ed11815c332f6ef6ba747c82c5ae60ded0e4b184a1d4a8120
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cab0f8e38f76a14251de705f0404ed443b9d597ddd93c3ba51a4a46f8ba8db5849d5819570034c95d70e9fbe95a4a4bf576df35564d0405c393eaaf7a1765e5
|
|
7
|
+
data.tar.gz: 2c9e36d50e6f304a018cac0deaba455421f67b5199e36b756dcffe9adb35fb443f9eaf09d8279231c3c85df10adbff1b88407a4d826cb2f3a7acb0a594c339ff
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<h1 align="center">
|
|
3
|
-
<h3 align="center">Autonomous Agentic Loop for
|
|
2
|
+
<h1 align="center">Ralph.rb</h1>
|
|
3
|
+
<h3 align="center">Autonomous Agentic Loop for OpenCode</h3>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="
|
|
7
|
+
<img src="ralph.jpg" alt="Ralph Wiggum - Iterative AI coding loop for Opencode" />
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
@@ -13,11 +13,6 @@
|
|
|
13
13
|
<em>Based on the <a href="https://ghuntley.com/ralph/">Ralph Wiggum technique</a> by Geoffrey Huntley</em>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
-
<p align="center">
|
|
17
|
-
<a href="https://github.com/Th0rgal/ralph-wiggum/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License"></a>
|
|
18
|
-
<a href="https://github.com/Th0rgal/ralph-wiggum"><img src="https://img.shields.io/badge/built%20with-Bun%20%2B%20TypeScript-f472b6.svg" alt="Built with Bun + TypeScript"></a>
|
|
19
|
-
<a href="https://github.com/Th0rgal/ralph-wiggum/releases"><img src="https://img.shields.io/github/v/release/Th0rgal/ralph-wiggum?include_prereleases" alt="Release"></a>
|
|
20
|
-
</p>
|
|
21
16
|
|
|
22
17
|
<p align="center">
|
|
23
18
|
<a href="#supported-agents">Supported Agents</a> •
|
data/lib/ralph/cli.rb
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ralph
|
|
4
|
-
# Command-line interface for ralph. Parses
|
|
5
|
-
#
|
|
4
|
+
# Command-line interface for ralph. Parses subcommands (build/plan),
|
|
5
|
+
# arguments, reads stdin when piped, and launches the Loop.
|
|
6
6
|
#
|
|
7
7
|
# Usage:
|
|
8
|
-
#
|
|
8
|
+
# ralph build "focus on auth" --model=opus-4.5 --max-iterations=10
|
|
9
|
+
# ralph plan "user authentication system"
|
|
10
|
+
# ralph --max-iterations=10 # equivalent to: ralph build --max-iterations=10
|
|
11
|
+
# cat prompt.md | ralph build --model=opus-4.5
|
|
9
12
|
class CLI
|
|
13
|
+
SUBCOMMANDS = %w[build plan].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :subcommand
|
|
16
|
+
|
|
10
17
|
def initialize(argv)
|
|
11
18
|
@argv = argv.dup
|
|
19
|
+
@subcommand = extract_subcommand
|
|
12
20
|
@options = {
|
|
13
21
|
model: nil,
|
|
14
22
|
max_iterations: nil,
|
|
@@ -20,84 +28,113 @@ module Ralph
|
|
|
20
28
|
|
|
21
29
|
def run
|
|
22
30
|
parse_options
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
$stderr.puts "Error: no prompt provided."
|
|
26
|
-
$stderr.puts "Usage: ralph \"your prompt\" [options]"
|
|
27
|
-
$stderr.puts " cat prompt.md | ralph [options]"
|
|
28
|
-
$stderr.puts
|
|
29
|
-
$stderr.puts parser.help
|
|
30
|
-
exit 1
|
|
31
|
-
else
|
|
32
|
-
@options[:prompt] = prompt
|
|
33
|
-
Ralph::Loop.new(@options).run
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
31
|
+
prompt_object = build_prompt_object
|
|
32
|
+
@options[:prompt] = prompt_object
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
Ralph::Loop.new(@options).run
|
|
39
35
|
|
|
40
|
-
def parse_options
|
|
41
|
-
parser.parse!(@argv)
|
|
42
36
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error
|
|
43
37
|
$stderr.puts "Error: #{error.message}"
|
|
44
38
|
$stderr.puts parser.help
|
|
45
39
|
exit 1
|
|
46
40
|
end
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
@parser ||= OptionParser.new do |option_parser|
|
|
50
|
-
option_parser.banner = "Usage: ralph [prompt] [options]"
|
|
51
|
-
option_parser.separator ""
|
|
52
|
-
option_parser.separator "Options:"
|
|
53
|
-
|
|
54
|
-
option_parser.on("--model=MODEL", "Model to use (e.g. opus-4.5)") do |value|
|
|
55
|
-
@options[:model] = value
|
|
56
|
-
end
|
|
42
|
+
private
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
def extract_subcommand
|
|
45
|
+
if @argv.first && SUBCOMMANDS.include?(@argv.first)
|
|
46
|
+
@argv.shift
|
|
47
|
+
else
|
|
48
|
+
"build"
|
|
60
49
|
end
|
|
50
|
+
end
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
52
|
+
def parse_options
|
|
53
|
+
parser.parse!(@argv)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parser
|
|
57
|
+
@parser ||= OptionParser.new do |option_parser|
|
|
58
|
+
option_parser.banner = "Usage: ralph [build|plan] [text] [options]"
|
|
59
|
+
option_parser.separator ""
|
|
60
|
+
option_parser.separator "Subcommands:"
|
|
61
|
+
option_parser.separator " build Implement tasks from the plan (default)"
|
|
62
|
+
option_parser.separator " plan Gap analysis and plan generation"
|
|
63
|
+
option_parser.separator ""
|
|
64
|
+
option_parser.separator "Options:"
|
|
65
|
+
|
|
66
|
+
option_parser.on("--model=MODEL", "Model to use (e.g. opus-4.5)") do |value|
|
|
67
|
+
@options[:model] = value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
option_parser.on("--max-iterations=N", Integer, "Maximum number of iterations") do |value|
|
|
71
|
+
@options[:max_iterations] = value
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
option_parser.on("--duration=SECONDS", Integer, "Maximum total duration in seconds") do |value|
|
|
75
|
+
@options[:duration] = value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
option_parser.on("--max-context=N", Integer, "Maximum context tokens before iteration restart") do |value|
|
|
79
|
+
@options[:max_context] = value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
option_parser.on("--completion=STRING", "Completion string the agent emits when done") do |value|
|
|
83
|
+
@options[:completion] = value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
option_parser.on("-h", "--help", "Show this help message") do
|
|
87
|
+
puts option_parser
|
|
88
|
+
exit 0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
option_parser.on("-v", "--version", "Show version") do
|
|
92
|
+
puts "ralph #{Ralph::VERSION}"
|
|
93
|
+
exit 0
|
|
94
|
+
end
|
|
64
95
|
end
|
|
96
|
+
end
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
end
|
|
98
|
+
def read_user_text
|
|
99
|
+
parts = []
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
# Read from stdin if data is being piped in
|
|
102
|
+
unless $stdin.tty?
|
|
103
|
+
stdin_content = $stdin.read
|
|
104
|
+
parts << stdin_content if stdin_content && !stdin_content.strip.empty?
|
|
72
105
|
end
|
|
73
106
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
exit 0
|
|
77
|
-
end
|
|
107
|
+
# Remaining positional arguments are the inline text
|
|
108
|
+
parts << @argv.join(" ") if @argv.any?
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
puts "ralph #{Ralph::VERSION}"
|
|
81
|
-
exit 0
|
|
82
|
-
end
|
|
110
|
+
parts.join("\n\n") if parts.any?
|
|
83
111
|
end
|
|
84
|
-
end
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
|
|
113
|
+
def build_prompt_object
|
|
114
|
+
user_text = read_user_text
|
|
115
|
+
completion = @options[:completion]
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
if @subcommand == "plan"
|
|
118
|
+
build_plan_prompt(user_text, completion)
|
|
119
|
+
else
|
|
120
|
+
build_build_prompt(user_text, completion)
|
|
121
|
+
end
|
|
93
122
|
end
|
|
94
123
|
|
|
95
|
-
|
|
96
|
-
|
|
124
|
+
def build_plan_prompt(user_text, completion)
|
|
125
|
+
Hash.new.then do |plan_options|
|
|
126
|
+
plan_options[:goal] = user_text if user_text
|
|
127
|
+
plan_options[:all_done] = completion if completion
|
|
128
|
+
Prompt::Plan.new(**plan_options)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
97
131
|
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
def build_build_prompt(user_text, completion)
|
|
133
|
+
Hash.new.then do |build_options|
|
|
134
|
+
build_options[:context] = user_text if user_text
|
|
135
|
+
build_options[:all_done] = completion if completion
|
|
136
|
+
Prompt::Build.new(**build_options)
|
|
137
|
+
end
|
|
100
138
|
end
|
|
101
|
-
end
|
|
102
139
|
end
|
|
103
140
|
end
|
data/lib/ralph/display.rb
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
# Represents a single execution cycle within the loop. Runs the agent with
|
|
5
|
+
# the prompt, monitors events, detects signals, and can be cancelled at
|
|
6
|
+
# any time. Tracks the outcome of the iteration (task-done, all-done,
|
|
7
|
+
# context guard, error, etc.).
|
|
8
|
+
class Iteration
|
|
9
|
+
OUTCOMES = %i[task_done all_done context_limit duration_limit error].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :number, :outcome
|
|
12
|
+
|
|
13
|
+
def initialize(number:, prompt_text:, model:, task_done_string:, all_done_string:, metrics:, display:)
|
|
14
|
+
@number = number
|
|
15
|
+
@prompt_text = prompt_text
|
|
16
|
+
@model = model
|
|
17
|
+
@task_done_string = task_done_string
|
|
18
|
+
@all_done_string = all_done_string
|
|
19
|
+
@metrics = metrics
|
|
20
|
+
@display = display
|
|
21
|
+
@outcome = nil
|
|
22
|
+
@agent = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Run the iteration. Yields control back to the loop via the check block
|
|
26
|
+
# which determines if the iteration should be cancelled due to external
|
|
27
|
+
# limits (context, duration). Returns the outcome symbol.
|
|
28
|
+
def run(max_context: nil, duration_exceeded: nil)
|
|
29
|
+
@agent = Opencode.new(model: @model)
|
|
30
|
+
|
|
31
|
+
@agent.run(@prompt_text) do |event|
|
|
32
|
+
@metrics.process(event)
|
|
33
|
+
@display.show_event(event)
|
|
34
|
+
|
|
35
|
+
if event.is_a?(Events::Text)
|
|
36
|
+
check_signals(event.text)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if @outcome
|
|
40
|
+
@agent.cancel
|
|
41
|
+
break
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if max_context && @metrics.current_context >= max_context
|
|
45
|
+
@outcome = :context_limit
|
|
46
|
+
@display.show_iteration_cancelled(
|
|
47
|
+
"context limit reached (#{@metrics.current_context}/#{max_context})"
|
|
48
|
+
)
|
|
49
|
+
@agent.cancel
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if duration_exceeded&.call
|
|
54
|
+
@outcome = :duration_limit
|
|
55
|
+
@display.show_iteration_cancelled("duration limit reached")
|
|
56
|
+
@agent.cancel
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
rescue Errno::ENOENT => error
|
|
61
|
+
@outcome = :error
|
|
62
|
+
@display.show_iteration_error("agent not found: #{error.message}")
|
|
63
|
+
rescue IOError, Errno::EPIPE => error
|
|
64
|
+
@outcome = :error
|
|
65
|
+
@display.show_iteration_error("agent communication error: #{error.message}")
|
|
66
|
+
rescue StandardError => error
|
|
67
|
+
@outcome = :error
|
|
68
|
+
@display.show_iteration_error("unexpected error: #{error.message}")
|
|
69
|
+
ensure
|
|
70
|
+
@outcome ||= :unknown
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Whether this iteration ended because the agent signaled task-done
|
|
74
|
+
def task_done?
|
|
75
|
+
@outcome == :task_done
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Whether this iteration ended because the agent signaled all-done
|
|
79
|
+
def all_done?
|
|
80
|
+
@outcome == :all_done
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Whether this iteration ended due to an error
|
|
84
|
+
def error?
|
|
85
|
+
@outcome == :error
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def check_signals(text)
|
|
91
|
+
if text
|
|
92
|
+
if text.include?(@all_done_string)
|
|
93
|
+
@outcome = :all_done
|
|
94
|
+
elsif @task_done_string && text.include?(@task_done_string)
|
|
95
|
+
@outcome = :task_done
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/ralph/loop.rb
CHANGED
|
@@ -4,21 +4,15 @@ module Ralph
|
|
|
4
4
|
# The core iteration engine. Runs opencode in a loop, restarting fresh
|
|
5
5
|
# iterations whenever context grows too large or time limits are hit.
|
|
6
6
|
# The loop ends when:
|
|
7
|
-
# - the agent emits the completion string
|
|
7
|
+
# - the agent emits the all-done completion string
|
|
8
8
|
# - max iterations are reached
|
|
9
9
|
# - total duration is exceeded
|
|
10
|
+
#
|
|
11
|
+
# An iteration ends early (and a fresh one begins) when:
|
|
12
|
+
# - the agent emits the task-done string (build mode only)
|
|
13
|
+
# - context limit is exceeded
|
|
10
14
|
class Loop
|
|
11
|
-
|
|
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
|
|
15
|
+
attr_reader :metrics, :iteration_number, :completed, :iteration_outcomes
|
|
22
16
|
|
|
23
17
|
def initialize(options)
|
|
24
18
|
@prompt = options[:prompt]
|
|
@@ -26,10 +20,12 @@ module Ralph
|
|
|
26
20
|
@max_iterations = options[:max_iterations]
|
|
27
21
|
@duration_limit = options[:duration]
|
|
28
22
|
@max_context = options[:max_context]
|
|
29
|
-
@
|
|
23
|
+
@all_done_string = resolve_all_done_string(options)
|
|
24
|
+
@task_done_string = resolve_task_done_string
|
|
30
25
|
@metrics = Metrics.new
|
|
31
26
|
@iteration_number = 0
|
|
32
27
|
@completed = false
|
|
28
|
+
@iteration_outcomes = []
|
|
33
29
|
@started_at = nil
|
|
34
30
|
@display = Display.new(self)
|
|
35
31
|
end
|
|
@@ -37,7 +33,7 @@ module Ralph
|
|
|
37
33
|
# Run the main loop until a termination condition is met.
|
|
38
34
|
def run
|
|
39
35
|
@started_at = now_seconds
|
|
40
|
-
@display.show_start(
|
|
36
|
+
@display.show_start(prompt_text)
|
|
41
37
|
|
|
42
38
|
loop do
|
|
43
39
|
break if should_stop_loop?
|
|
@@ -46,9 +42,14 @@ module Ralph
|
|
|
46
42
|
@metrics.new_iteration
|
|
47
43
|
@display.show_iteration_start
|
|
48
44
|
|
|
49
|
-
run_iteration
|
|
45
|
+
iteration = run_iteration
|
|
50
46
|
|
|
47
|
+
@iteration_outcomes << { number: iteration.number, outcome: iteration.outcome }
|
|
51
48
|
@display.show_iteration_end
|
|
49
|
+
|
|
50
|
+
if iteration.all_done?
|
|
51
|
+
@completed = true
|
|
52
|
+
end
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
@display.show_summary
|
|
@@ -66,70 +67,67 @@ module Ralph
|
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@display.show_termination("duration limit reached (#{@duration_limit}s)")
|
|
77
|
-
true
|
|
70
|
+
# Read all-done string from the prompt object if it responds to it,
|
|
71
|
+
# falling back to the CLI --completion option or the default.
|
|
72
|
+
def resolve_all_done_string(options)
|
|
73
|
+
if options[:completion]
|
|
74
|
+
options[:completion]
|
|
75
|
+
elsif @prompt.respond_to?(:all_done) && @prompt.all_done
|
|
76
|
+
@prompt.all_done
|
|
78
77
|
else
|
|
79
|
-
|
|
78
|
+
Prompt::Build::DEFAULT_ALL_DONE
|
|
80
79
|
end
|
|
81
80
|
end
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@display.show_event(event)
|
|
91
|
-
|
|
92
|
-
if event.is_a?(Events::Text)
|
|
93
|
-
check_completion(event.text)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if should_cancel_iteration?(iteration_started_at)
|
|
97
|
-
@display.show_iteration_cancelled(cancel_reason(iteration_started_at))
|
|
98
|
-
agent.cancel
|
|
99
|
-
break
|
|
100
|
-
end
|
|
82
|
+
# Read task-done string from the prompt object. Plan prompts return nil,
|
|
83
|
+
# meaning the loop will not watch for task-done signals.
|
|
84
|
+
def resolve_task_done_string
|
|
85
|
+
if @prompt.respond_to?(:task_done)
|
|
86
|
+
@prompt.task_done
|
|
87
|
+
else
|
|
88
|
+
nil
|
|
101
89
|
end
|
|
102
90
|
end
|
|
103
91
|
|
|
104
|
-
def
|
|
105
|
-
|
|
92
|
+
def prompt_text
|
|
93
|
+
@prompt.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def should_stop_loop?
|
|
97
|
+
if @completed
|
|
98
|
+
true
|
|
99
|
+
elsif @max_iterations && @iteration_number >= @max_iterations
|
|
100
|
+
@display.show_termination("max iterations reached (#{@max_iterations})")
|
|
106
101
|
true
|
|
107
|
-
elsif
|
|
102
|
+
elsif duration_exceeded?
|
|
103
|
+
@display.show_termination("duration limit reached (#{@duration_limit}s)")
|
|
108
104
|
true
|
|
109
105
|
else
|
|
110
106
|
false
|
|
111
107
|
end
|
|
112
108
|
end
|
|
113
109
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@
|
|
127
|
-
|
|
110
|
+
def run_iteration
|
|
111
|
+
iteration = Iteration.new(
|
|
112
|
+
number: @iteration_number,
|
|
113
|
+
prompt_text: prompt_text,
|
|
114
|
+
model: @model,
|
|
115
|
+
task_done_string: @task_done_string,
|
|
116
|
+
all_done_string: @all_done_string,
|
|
117
|
+
metrics: @metrics,
|
|
118
|
+
display: @display
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
iteration.run(
|
|
122
|
+
max_context: @max_context,
|
|
123
|
+
duration_exceeded: -> { duration_exceeded? }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
iteration
|
|
128
127
|
end
|
|
129
128
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
"#{system_instructions}\n\n---\n\n#{@prompt}"
|
|
129
|
+
def duration_exceeded?
|
|
130
|
+
@duration_limit && elapsed_seconds >= @duration_limit
|
|
133
131
|
end
|
|
134
132
|
|
|
135
133
|
def now_seconds
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
module Prompt
|
|
5
|
+
# The building prompt -- the heart of ralph. Instructs the agent to study
|
|
6
|
+
# specs, read the plan, pick ONE task, implement it, validate, update the
|
|
7
|
+
# plan, commit, and signal task-done.
|
|
8
|
+
#
|
|
9
|
+
# Responds to #to_s returning the complete prompt text for one iteration.
|
|
10
|
+
class Build
|
|
11
|
+
DEFAULT_TASK_DONE = "<task>DONE</task>"
|
|
12
|
+
DEFAULT_ALL_DONE = "<promise>COMPLETE</promise>"
|
|
13
|
+
|
|
14
|
+
attr_reader :task_done, :all_done
|
|
15
|
+
|
|
16
|
+
def initialize(task_done: DEFAULT_TASK_DONE, all_done: DEFAULT_ALL_DONE, context: nil)
|
|
17
|
+
@task_done = task_done
|
|
18
|
+
@all_done = all_done
|
|
19
|
+
@context = context
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
prompt = format(TEMPLATE, task_done: @task_done, all_done: @all_done)
|
|
24
|
+
if @context && !@context.strip.empty?
|
|
25
|
+
"#{prompt}\n\n---\n\n#{@context}"
|
|
26
|
+
else
|
|
27
|
+
prompt
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
TEMPLATE = <<~PROMPT
|
|
34
|
+
0a. Study `specs/*` to learn the application specifications.
|
|
35
|
+
0b. Study the plans in `plans/`.
|
|
36
|
+
0c. For reference, the application source code is in `lib/*`.
|
|
37
|
+
|
|
38
|
+
1. Your task is to implement ONE item from the plans. Follow the plans in `plans/` and choose the most important item to address. Before making changes, search the codebase (don't assume not implemented).
|
|
39
|
+
2. After implementing functionality or resolving problems, run the tests and checks for that unit of code that was improved. If functionality is missing then it's your job to add it as per the application specifications. Think carefully.
|
|
40
|
+
3. When you discover issues, immediately update the plans in `plans/` with your findings. When resolved, update and remove the item.
|
|
41
|
+
4. When the tests pass, update the plans in `plans/`, then `git add -A` then `git commit` with a message describing the changes. After the commit, `git push`.
|
|
42
|
+
5. After committing, output the task-done signal on its own line: %{task_done}
|
|
43
|
+
6. If there are no remaining items in the plans, output the all-done signal instead: %{all_done}
|
|
44
|
+
|
|
45
|
+
99999. Important: When authoring documentation, capture the why -- tests and implementation importance.
|
|
46
|
+
999999. Important: Single sources of truth, no migrations/adapters. If tests unrelated to your work fail, resolve them as part of the increment.
|
|
47
|
+
9999999. As soon as there are no build or test errors create a git tag. If there are no git tags start at 0.0.0 and increment patch by 1 for example 0.0.1 if 0.0.0 does not exist.
|
|
48
|
+
99999999. You may add extra logging if required to debug issues.
|
|
49
|
+
999999999. Keep the plans in `plans/` current with learnings -- future work depends on this to avoid duplicating efforts. Update especially after finishing your turn.
|
|
50
|
+
9999999999. When you learn something new about how to run the application, update @AGENTS.md but keep it brief. For example if you run commands multiple times before learning the correct command then that file should be updated.
|
|
51
|
+
99999999999. For any bugs you notice, resolve them or document them in the plans even if it is unrelated to the current piece of work.
|
|
52
|
+
999999999999. Implement functionality completely. Placeholders and stubs waste efforts and time redoing the same work.
|
|
53
|
+
9999999999999. When plans become large periodically clean out the items that are completed.
|
|
54
|
+
99999999999999. If you find inconsistencies in the specs/* then carefully update the specs.
|
|
55
|
+
999999999999999. IMPORTANT: Keep @AGENTS.md operational only -- status updates and progress notes belong in the plans. A bloated AGENTS.md pollutes every future loop's context.
|
|
56
|
+
9999999999999999. IMPORTANT: Do ONE task per iteration. Pick it, do it, commit it, signal task-done. Do not continue to the next task -- the loop will start a fresh iteration for that.
|
|
57
|
+
PROMPT
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
module Prompt
|
|
5
|
+
# The planning prompt -- instructs the agent to study specs, compare against
|
|
6
|
+
# code, and produce a prioritized task list in plans/. It does NOT implement
|
|
7
|
+
# anything. It does NOT commit anything.
|
|
8
|
+
#
|
|
9
|
+
# Responds to #to_s returning the complete prompt text for one iteration.
|
|
10
|
+
class Plan
|
|
11
|
+
DEFAULT_ALL_DONE = "<promise>COMPLETE</promise>"
|
|
12
|
+
DEFAULT_GOAL = "Consider missing elements and plan accordingly. " \
|
|
13
|
+
"If an element is missing, search first to confirm it doesn't exist, " \
|
|
14
|
+
"then if needed author the specification at specs/FILENAME.md."
|
|
15
|
+
|
|
16
|
+
attr_reader :all_done
|
|
17
|
+
|
|
18
|
+
def initialize(goal: nil, all_done: DEFAULT_ALL_DONE)
|
|
19
|
+
@goal = goal && !goal.strip.empty? ? goal : DEFAULT_GOAL
|
|
20
|
+
@all_done = all_done
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Plan mode has no task-done signal
|
|
24
|
+
def task_done
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
format(TEMPLATE, goal: @goal, all_done: @all_done)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
TEMPLATE = <<~PROMPT
|
|
35
|
+
0a. Study `specs/*` to learn the application specifications.
|
|
36
|
+
0b. Study the plans in `plans/` (if present) to understand the plan so far.
|
|
37
|
+
0c. Study `lib/*` to understand the existing codebase and shared utilities.
|
|
38
|
+
|
|
39
|
+
1. Study the plans in `plans/` (if present; they may be incorrect) and study existing source code in `lib/*` and compare it against `specs/*`. Analyze findings, prioritize tasks, and create/update the plans in `plans/` as bullet point lists sorted in priority of items yet to be implemented. Think carefully. Consider searching for TODO, minimal implementations, placeholders, skipped/flaky tests, and inconsistent patterns. Study the plans to determine the starting point for research and keep them up to date with items considered complete/incomplete.
|
|
40
|
+
|
|
41
|
+
IMPORTANT: Plan only. Do NOT implement anything. Do NOT assume functionality is missing; confirm with code search first.
|
|
42
|
+
|
|
43
|
+
ULTIMATE GOAL: %{goal}
|
|
44
|
+
|
|
45
|
+
When you have completed the plan, output the exact completion string on its own line: %{all_done}
|
|
46
|
+
PROMPT
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/ralph/version.rb
CHANGED