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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3950370724ceca76366a10ef15914ab10abc209595d1a5c8ddc466c071ea032
4
- data.tar.gz: 0f85bc6a1f1a00d98ee6940928ce3ba506f6591536e14e4eb0cd30c3e6e914db
3
+ metadata.gz: 5554c13454fc43d04f41b7f969653cb7d51243eb40df245aefad31d82b38ab3a
4
+ data.tar.gz: 97909bf34b37d86ed11815c332f6ef6ba747c82c5ae60ded0e4b184a1d4a8120
5
5
  SHA512:
6
- metadata.gz: 181d38c7c1e72fd04ac4b22118a09caac7f262d7037d4e1c42f4f3440bac0b6a3c3b08d48934b2083ec3972a241457a18770c3f6c0038d82bd3ae33afd790416
7
- data.tar.gz: 1feee430c07868a4877f351c1dcef388f42b99cb1db33b1c7515e22aeda33dba7ac0ce1abd5568c1315ded580f674f48f394909db2267d26bc9a64092e8d62e9
6
+ metadata.gz: 6cab0f8e38f76a14251de705f0404ed443b9d597ddd93c3ba51a4a46f8ba8db5849d5819570034c95d70e9fbe95a4a4bf576df35564d0405c393eaaf7a1765e5
7
+ data.tar.gz: 2c9e36d50e6f304a018cac0deaba455421f67b5199e36b756dcffe9adb35fb443f9eaf09d8279231c3c85df10adbff1b88407a4d826cb2f3a7acb0a594c339ff
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ralph.rb (1.2.435535439)
4
+ ralph.rb (2.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <p align="center">
2
- <h1 align="center">Open Ralph Wiggum</h1>
3
- <h3 align="center">Autonomous Agentic Loop for Claude Code, Codex & OpenCode</h3>
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="screenshot.webp" alt="Open Ralph Wiggum - Iterative AI coding loop for Claude Code and Codex" />
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 arguments, reads stdin when piped,
5
- # combines everything into a prompt, and launches the Loop.
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
- # cat prompt.md | ralph "extra instructions" --model=opus-4.5 --max-iterations=10
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
- build_prompt.then do |prompt|
24
- if prompt.nil? || prompt.strip.empty?
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
- private
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
- def parser
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
- option_parser.on("--max-iterations=N", Integer, "Maximum number of iterations") do |value|
59
- @options[:max_iterations] = value
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
- option_parser.on("--duration=SECONDS", Integer, "Maximum total duration in seconds") do |value|
63
- @options[:duration] = value
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
- option_parser.on("--max-context=N", Integer, "Maximum context tokens before iteration restart") do |value|
67
- @options[:max_context] = value
68
- end
98
+ def read_user_text
99
+ parts = []
69
100
 
70
- option_parser.on("--completion=STRING", "Completion string the agent emits when done") do |value|
71
- @options[:completion] = value
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
- option_parser.on("-h", "--help", "Show this help message") do
75
- puts option_parser
76
- exit 0
77
- end
107
+ # Remaining positional arguments are the inline text
108
+ parts << @argv.join(" ") if @argv.any?
78
109
 
79
- option_parser.on("-v", "--version", "Show version") do
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
- def build_prompt
87
- parts = []
113
+ def build_prompt_object
114
+ user_text = read_user_text
115
+ completion = @options[:completion]
88
116
 
89
- # Read from stdin if data is being piped in
90
- unless $stdin.tty?
91
- stdin_content = $stdin.read
92
- parts << stdin_content if stdin_content && !stdin_content.strip.empty?
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
- # Remaining positional arguments are the inline prompt
96
- parts << @argv.join(" ") if @argv.any?
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
- if parts.any?
99
- parts.join("\n\n")
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
@@ -60,6 +60,11 @@ module Ralph
60
60
  puts " ** Iteration cancelled: #{reason}"
61
61
  end
62
62
 
63
+ def show_iteration_error(message)
64
+ puts
65
+ puts " ** Iteration error: #{message}"
66
+ end
67
+
63
68
  def show_termination(reason)
64
69
  puts
65
70
  puts " ** Loop terminated: #{reason}"
@@ -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
- 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
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
- @completion_string = options[:completion] || "<promise>COMPLETE</promise>"
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(@prompt)
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
- 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
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
- false
78
+ Prompt::Build::DEFAULT_ALL_DONE
80
79
  end
81
80
  end
82
81
 
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)
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 should_cancel_iteration?(iteration_started_at)
105
- if @max_context && @metrics.current_context >= @max_context
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 @duration_limit && elapsed_seconds >= @duration_limit
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 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"
121
- end
122
- end
123
-
124
- def check_completion(text)
125
- if text && text.include?(@completion_string)
126
- @completed = true
127
- end
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 build_prompt
131
- system_instructions = format(SYSTEM_PROMPT, completion: @completion_string)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ralph
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.0"
5
5
  end