rralph 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 879adb96bb31b3269ef4f60a200a8689cf4283f694f2aca5e0f9a95c24bef7ff
4
+ data.tar.gz: 909224ae6b25857bff64c7c789d66a16025c74601ef397d9f40c8d066707ba94
5
+ SHA512:
6
+ metadata.gz: 6411d979dddce3cd5de21d510d5af092c4aa65f0d6af5371c96628320b3658d1b2f5a70495e861c15f77044fc1486fe1de29eb9fb0383a6a6637fe61c4cb11e8
7
+ data.tar.gz: '07018c95c8ec9029893b4548ede3e5ef1a1e601bc1243c665ba053c882fdca83ac7523c71df6005c179a85af3f775ef42f9d6a4b997ea6490dc4753918f0748b'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Hagege
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # rralph
2
+
3
+ A self-improving task orchestrator for AI-assisted development. Based on Ralph Wiggum concept.
4
+
5
+ ## Overview
6
+
7
+ `rralph` automates an iterative, AI-assisted development workflow. It reads your project's plan, learnings, and todo list, then orchestrates AI tool invocations to complete tasks one by one — learning, testing, and committing along the way.
8
+
9
+ ## Installation
10
+
11
+ ```
12
+ gem install rralph
13
+ ```
14
+
15
+ Ralph by default uses `qwen-code` as the AI agent. You can override this with the `--ai-command` flag.
16
+
17
+ ## Usage
18
+
19
+ ### Prerequisites
20
+
21
+ Before running `rralph`, ensure you have:
22
+
23
+ 1. A Git repository initialized
24
+ 2. Three Markdown files in your working directory:
25
+ - `plan.md` — Your high-level project plan
26
+ - `learnings.md` — Accumulated insights (can start empty)
27
+ - `todo.md` — Task list with checkboxes (one task per line, can be empty first, rralph will generate the tasks)
28
+
29
+ Example `plan.md`:
30
+
31
+ ```markdown
32
+ Plan: Bash script that prints "IS EVEN" or "IS ODD" based on input.
33
+
34
+ Input: One integer as command-line argument.
35
+ Output: "IS EVEN" if divisible by 2, "IS ODD" otherwise.
36
+ Error: If no argument or non-integer, print error and exit 1.
37
+
38
+ Logic: Use % to check remainder. Validate input with regex: ^-?[0-9]+$.
39
+
40
+ File: odd_even.sh
41
+ Requirements:
42
+
43
+ - Need to be in bash
44
+ - Validate input
45
+ - Use conditional with (( ))
46
+ - No external tools
47
+
48
+ Test: 0→EVEN, 7→ODD, -4→EVEN, abc→error, no arg→error
49
+ ```
50
+
51
+ ### Basic Usage
52
+
53
+ Run `rralph` with default settings:
54
+
55
+ ```bash
56
+ rralph
57
+ ```
58
+
59
+ Or with options:
60
+
61
+ ```bash
62
+ rralph --max-failures 2 --watch
63
+ ```
64
+
65
+ ### Command-Line Options
66
+
67
+ ```
68
+ rralph run [OPTIONS]
69
+
70
+ Options:
71
+ -m, --max-failures N Maximum allowed failures before stopping (default: 3)
72
+ -a, --ai-command "CMD" AI command to invoke (default: "qwen-code -y -s")
73
+ -w, --watch Run in continuous loop until completion or max failures
74
+ -p, --plan-path PATH Path to plan.md file (default: "plan.md")
75
+ -l, --learnings-path PATH Path to learnings.md file (default: "learnings.md")
76
+ -t, --todo-path PATH Path to todo.md file (default: "todo.md")
77
+ -h, --help Show help message
78
+ ```
79
+
80
+ ### Examples
81
+
82
+ Run a single cycle:
83
+
84
+ ```bash
85
+ rralph
86
+ ```
87
+
88
+ Run continuously until all tasks are done or max failures reached:
89
+
90
+ ```bash
91
+ rralph --watch --max-failures 5
92
+ ```
93
+
94
+ Use a custom AI command:
95
+
96
+ ```bash
97
+ rralph --ai-command "claude --prompt"
98
+ ```
99
+
100
+ View progress statistics:
101
+
102
+ ```bash
103
+ $> rralph stats
104
+ Tasks: 5/6 done
105
+ Pending: 1
106
+ Learnings: 6 lines
107
+ ```
108
+
109
+ ## How It Works
110
+
111
+ 1. **Read** — `rralph` reads `plan.md`, `learnings.md`, and `todo.md`
112
+ 2. **Prompt** — Builds a prompt with file contents and sends to LLM
113
+ 3. **Parse** — Analyzes AI response for:
114
+ - `FAILURE` keyword (case-insensitive, whole word)
115
+ - New learnings to extract
116
+ 4. **Update** — On success:
117
+ - Marks current task as complete in `todo.md`
118
+ - Appends new learnings to `learnings.md`
119
+ - Commits all changes to Git
120
+ 5. **Repeat** — In `--watch` mode, continues until done or max failures
121
+
122
+ ### Failure Handling
123
+
124
+ - Each `FAILURE` response increments a counter
125
+ - Non-failure responses reset the counter to 0
126
+ - When max failures reached, `rralph` exits with error:
127
+ - ```
128
+ Max failures reached (N). Stopping to avoid infinite loops. Review learnings.md and todo.md.
129
+ ```
130
+
131
+ ### Logging
132
+
133
+ Human-readable logs are output to stderr:
134
+
135
+ ```
136
+ ✅ [Cycle 4] Task completed. 0 failures. Git commit: abc123
137
+ ❌ [Cycle 5] FAILURE detected. Failures: 2/3
138
+ ```
139
+
140
+ In `--watch` mode, AI responses are saved to `logs/` for audit trail.
141
+
142
+ ## License
143
+
144
+ Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/rralph ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/rralph"
4
+
5
+ Rralph::CLI.start(ARGV)
data/lib/rralph/cli.rb ADDED
@@ -0,0 +1,104 @@
1
+ require "thor"
2
+ require_relative "runner"
3
+
4
+ module Rralph
5
+ class CLI < Thor
6
+ desc "start", "Run the rralph orchestrator"
7
+ method_option :max_failures,
8
+ type: :numeric,
9
+ default: 3,
10
+ aliases: "-m",
11
+ desc: "Maximum allowed failures before stopping"
12
+ method_option :ai_command,
13
+ type: :string,
14
+ default: "qwen-code -y -s",
15
+ aliases: "-a",
16
+ desc: "AI command to invoke"
17
+ method_option :watch,
18
+ type: :boolean,
19
+ default: false,
20
+ aliases: "-w",
21
+ desc: "Run in continuous loop until completion or max failures"
22
+ method_option :plan_path,
23
+ type: :string,
24
+ default: "plan.md",
25
+ aliases: "-p",
26
+ desc: "Path to plan.md file"
27
+ method_option :learnings_path,
28
+ type: :string,
29
+ default: "learnings.md",
30
+ aliases: "-l",
31
+ desc: "Path to learnings.md file"
32
+ method_option :todo_path,
33
+ type: :string,
34
+ default: "todo.md",
35
+ aliases: "-t",
36
+ desc: "Path to todo.md file"
37
+
38
+ def start
39
+ runner = Runner.new(
40
+ max_failures: options[:max_failures],
41
+ ai_command: options[:ai_command],
42
+ watch: options[:watch],
43
+ plan_path: options[:plan_path],
44
+ learnings_path: options[:learnings_path],
45
+ todo_path: options[:todo_path]
46
+ )
47
+
48
+ runner.run
49
+ rescue Rralph::FileNotFound => e
50
+ $stderr.puts "Error: #{e.message}"
51
+ $stderr.puts "Please ensure plan.md, learnings.md, and todo.md exist in the current directory."
52
+ exit 1
53
+ rescue Rralph::GitError => e
54
+ $stderr.puts "Git Error: #{e.message}"
55
+ exit 1
56
+ rescue => e
57
+ $stderr.puts "Unexpected error: #{e.message}"
58
+ $stderr.puts e.backtrace.first(5)
59
+ exit 1
60
+ end
61
+
62
+ desc "version", "Show rralph version"
63
+ def version
64
+ puts "rralph v#{Rralph::VERSION}"
65
+ end
66
+
67
+ desc "stats", "Show progress statistics"
68
+ method_option :plan_path,
69
+ type: :string,
70
+ default: "plan.md",
71
+ aliases: "-p"
72
+ method_option :learnings_path,
73
+ type: :string,
74
+ default: "learnings.md",
75
+ aliases: "-l"
76
+ method_option :todo_path,
77
+ type: :string,
78
+ default: "todo.md",
79
+ aliases: "-t"
80
+
81
+ def stats
82
+ parser = Parser.new(
83
+ plan_path: options[:plan_path],
84
+ learnings_path: options[:learnings_path],
85
+ todo_path: options[:todo_path]
86
+ )
87
+
88
+ parser.load_files
89
+
90
+ all_tasks = parser.all_tasks
91
+ completed = parser.completed_tasks
92
+ pending = parser.pending_tasks
93
+
94
+ puts "Tasks: #{completed.size}/#{all_tasks.size} done"
95
+ puts "Pending: #{pending.size}"
96
+ puts "Learnings: #{parser.learnings_content.lines.size} lines"
97
+ rescue Rralph::FileNotFound => e
98
+ $stderr.puts "Error: #{e.message}"
99
+ exit 1
100
+ end
101
+
102
+ default_task :start
103
+ end
104
+ end
@@ -0,0 +1,48 @@
1
+ module Rralph
2
+ class FileUpdater
3
+ def initialize(todo_path: "todo.md", learnings_path: "learnings.md")
4
+ @todo_path = todo_path
5
+ @learnings_path = learnings_path
6
+ end
7
+
8
+ def mark_task_completed(task_index)
9
+ content = File.read(@todo_path)
10
+ lines = content.lines
11
+
12
+ line = lines[task_index]
13
+ if line
14
+ updated_line = line.gsub(/^([-*]) \[ \]/, '\1 [x]')
15
+ lines[task_index] = updated_line
16
+ File.write(@todo_path, lines.join)
17
+ end
18
+ end
19
+
20
+ def append_learnings(new_learnings)
21
+ return if new_learnings.empty?
22
+
23
+ existing_content = File.exist?(@learnings_path) ? File.read(@learnings_path) : ""
24
+ existing_learnings = existing_content.lines.map(&:strip).reject(&:empty?)
25
+
26
+ unique_learnings = new_learnings.reject do |learning|
27
+ existing_learnings.any? { |existing| existing.include?(learning) || learning.include?(existing) }
28
+ end
29
+
30
+ return if unique_learnings.empty?
31
+
32
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
33
+ new_section = "\n\n## Learnings - #{timestamp}\n\n"
34
+ unique_learnings.each do |learning|
35
+ new_section += "- #{learning}\n"
36
+ end
37
+
38
+ File.write(@learnings_path, existing_content.rstrip + new_section + "\n")
39
+ end
40
+
41
+ def todo_empty?
42
+ return true unless File.exist?(@todo_path)
43
+
44
+ content = File.read(@todo_path)
45
+ content.lines.none? { |line| line.match?(/^[-*] \[ \]/i) }
46
+ end
47
+ end
48
+ end
data/lib/rralph/git.rb ADDED
@@ -0,0 +1,37 @@
1
+ require "shellwords"
2
+
3
+ module Rralph
4
+ class Git
5
+ def initialize
6
+ end
7
+
8
+ def in_git_repo?
9
+ result = `git rev-parse --git-dir 2>/dev/null`
10
+ $?.success?
11
+ end
12
+
13
+ def commit_changes(message)
14
+ add_result = `git add . 2>&1`
15
+ unless $?.success?
16
+ raise GitError, "Failed to stage files: #{add_result}"
17
+ end
18
+
19
+ commit_result = `git commit -m #{message.shellescape} 2>&1`
20
+ unless $?.success?
21
+ raise GitError, "Failed to commit: #{commit_result}"
22
+ end
23
+
24
+ sha = `git rev-parse --short HEAD 2>/dev/null`.strip
25
+ sha
26
+ end
27
+
28
+ def status
29
+ `git status --short 2>/dev/null`
30
+ end
31
+
32
+ def has_changes?
33
+ status = `git status --porcelain 2>/dev/null`
34
+ !status.empty?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,126 @@
1
+ module Rralph
2
+ class Parser
3
+ attr_reader :plan_content, :learnings_content, :todo_content
4
+
5
+ def initialize(plan_path: "plan.md", learnings_path: "learnings.md", todo_path: "todo.md")
6
+ @plan_path = plan_path
7
+ @learnings_path = learnings_path
8
+ @todo_path = todo_path
9
+ end
10
+
11
+ def load_files
12
+ raise FileNotFound, "plan.md not found" unless File.exist?(@plan_path)
13
+
14
+ @plan_content = File.read(@plan_path)
15
+ @learnings_content = File.exist?(@learnings_path) ? File.read(@learnings_path) : ""
16
+ @todo_content = File.exist?(@todo_path) ? File.read(@todo_path) : ""
17
+ end
18
+
19
+ def pending_tasks
20
+ return [] unless @todo_content
21
+
22
+ @todo_content.lines.map.with_index do |line, index|
23
+ stripped = line.strip
24
+ next unless stripped.start_with?("- [ ]") || stripped.start_with?("* [ ]")
25
+
26
+ task_text = stripped.sub(/^[-*] \[ \] /, "").strip
27
+ { index: index, line: line, text: task_text, raw: stripped }
28
+ end.compact
29
+ end
30
+
31
+ def completed_tasks
32
+ return [] unless @todo_content
33
+
34
+ @todo_content.lines.map.with_index do |line, index|
35
+ stripped = line.strip
36
+ next unless stripped.start_with?("- [x]") || stripped.start_with?("* [x]")
37
+
38
+ task_text = stripped.sub(/^[-*] \[x\] /i, "").strip
39
+ { index: index, line: line, text: task_text, raw: stripped }
40
+ end.compact
41
+ end
42
+
43
+ def all_tasks
44
+ return [] unless @todo_content
45
+
46
+ @todo_content.lines.map.with_index do |line, index|
47
+ stripped = line.strip
48
+ next unless stripped.match?(/^[-*] \[[ x]\]/i)
49
+
50
+ completed = stripped.match?(/^[-*] \[x\]/i)
51
+ task_text = stripped.sub(/^[-*] \[[ x]\] /i, "").strip
52
+ { index: index, line: line, text: task_text, raw: stripped, completed: completed }
53
+ end.compact
54
+ end
55
+
56
+ def has_pending_tasks?
57
+ pending_tasks.any?
58
+ end
59
+
60
+ def failure_detected?(response)
61
+ response.match?(/\bFAILURE\b/i)
62
+ end
63
+
64
+ def extract_learnings(response)
65
+ learnings = []
66
+
67
+ # Match various learning patterns like:
68
+ # "Learning: xyz", "**Learning to add:** xyz", "- Learning: xyz", etc.
69
+ response.lines.each do |line|
70
+ if match = line.match(/(?:^|\s)(?:\*\*)?(?:Learning|Insight|Note|Tip|Discovered|Found|Realized)[\s*]*[:\s]*(?:to add)?[:\s]*(.+?)(?:\*\*)?(?:\s*$)/i)
71
+ learning = match[1].strip
72
+ learning = learning.gsub(/^\*\*|\*\*$/, "").strip
73
+ learnings << learning unless learning.empty?
74
+ end
75
+ end
76
+
77
+ # Also extract from ## Learnings sections
78
+ if response.match?(/##?\s+Learnings/i)
79
+ section = response.split(/##?\s+Learnings/i)[1]
80
+ section = section.split(/##?\s+/)[0] if section.match?(/##?\s+/)
81
+ section.lines.each do |line|
82
+ stripped = line.strip
83
+ next if stripped.empty?
84
+ stripped = stripped.sub(/^[-*]\s*/, "")
85
+ learnings << stripped unless stripped.empty?
86
+ end
87
+ end
88
+
89
+ learnings.uniq
90
+ end
91
+
92
+ def build_prompt(current_task: nil)
93
+ <<~PROMPT
94
+ You are in an iterative development loop. There is a todo list with tasks.
95
+
96
+ YOUR CURRENT TASK (the first unchecked item in todo.md):
97
+ > #{current_task}
98
+
99
+ INSTRUCTIONS - Follow these steps in order:
100
+ 1. Implement ONLY the task shown above
101
+ 2. Write a unit test for it
102
+ 3. Run the test
103
+ 4. Respond with exactly one of:
104
+ - "DONE" if the task is complete and test passes
105
+ - "FAILURE" if the test fails after your best effort
106
+ 5. Optionally add learnings as: "Learning: <insight>"
107
+
108
+ IMPORTANT RULES:
109
+ - Work on ONE task only - the one shown above
110
+ - Do NOT implement other tasks from the todo list
111
+ - Do NOT mark tasks as done yourself
112
+ - After you respond "DONE", the system will mark this task complete
113
+ - Then you will receive the next task
114
+
115
+ --- plan.md (context) ---
116
+ #{@plan_content}
117
+
118
+ --- learnings.md (prior knowledge) ---
119
+ #{@learnings_content}
120
+
121
+ --- todo.md (full list - work on first unchecked only) ---
122
+ #{@todo_content}
123
+ PROMPT
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,250 @@
1
+ module Rralph
2
+ class Runner
3
+ attr_reader :cycle_count, :failure_count, :max_failures
4
+
5
+ def initialize(
6
+ max_failures: 3,
7
+ ai_command: "qwen-code -y -s",
8
+ watch: false,
9
+ plan_path: "plan.md",
10
+ learnings_path: "learnings.md",
11
+ todo_path: "todo.md"
12
+ )
13
+ @max_failures = max_failures
14
+ @ai_command = ai_command
15
+ @watch = watch
16
+ @plan_path = plan_path
17
+ @learnings_path = learnings_path
18
+ @todo_path = todo_path
19
+
20
+ @cycle_count = 0
21
+ @failure_count = 0
22
+
23
+ @parser = Parser.new(
24
+ plan_path: @plan_path,
25
+ learnings_path: @learnings_path,
26
+ todo_path: @todo_path
27
+ )
28
+ @file_updater = FileUpdater.new(
29
+ todo_path: @todo_path,
30
+ learnings_path: @learnings_path
31
+ )
32
+ @git = Git.new
33
+ end
34
+
35
+ def run
36
+ log("Starting rralph with max_failures=#{@max_failures}, ai_command='#{@ai_command}'")
37
+
38
+ unless @git.in_git_repo?
39
+ raise GitError, "Not in a git repository. Please initialize git first."
40
+ end
41
+
42
+ @parser.load_files
43
+
44
+ if todo_empty_or_missing?
45
+ log("todo.md is empty or missing. Generating todo list from plan...")
46
+ generate_todo_from_plan
47
+ return true
48
+ end
49
+
50
+ unless @parser.has_pending_tasks?
51
+ log("All tasks completed! Well done!")
52
+ return true
53
+ end
54
+
55
+ if @watch
56
+ run_watch_loop
57
+ else
58
+ run_single_cycle
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def run_watch_loop
65
+ loop do
66
+ success = run_single_cycle
67
+ break unless success
68
+ break unless @parser.has_pending_tasks?
69
+ break if @failure_count >= @max_failures
70
+
71
+ sleep 1
72
+ end
73
+
74
+ check_final_state
75
+ end
76
+
77
+ def run_single_cycle
78
+ @cycle_count += 1
79
+
80
+ @parser.load_files
81
+
82
+ unless @parser.has_pending_tasks?
83
+ log("All tasks completed! Well done!")
84
+ return false
85
+ end
86
+
87
+ pending = @parser.pending_tasks
88
+ current_task = pending.first
89
+
90
+ log("Cycle #{@cycle_count}: Processing task: #{current_task[:text]}")
91
+
92
+ prompt = @parser.build_prompt(current_task: current_task[:text])
93
+ response = execute_ai_command(prompt)
94
+
95
+ # Always save AI request/response to logs for debugging
96
+ save_ai_log(prompt, response)
97
+
98
+ if response.nil?
99
+ @failure_count += 1
100
+ log("❌ [Cycle #{@cycle_count}] AI command failed. Failures: #{@failure_count}/#{@max_failures}")
101
+ return handle_failure
102
+ end
103
+
104
+ if @parser.failure_detected?(response)
105
+ @failure_count += 1
106
+ log("❌ [Cycle #{@cycle_count}] FAILURE detected. Failures: #{@failure_count}/#{@max_failures}")
107
+ return handle_failure
108
+ end
109
+
110
+ @failure_count = 0
111
+ log("✅ [Cycle #{@cycle_count}] Task completed. #{@failure_count} failures.")
112
+
113
+ @file_updater.mark_task_completed(current_task[:index])
114
+
115
+ new_learnings = @parser.extract_learnings(response)
116
+ @file_updater.append_learnings(new_learnings) if new_learnings.any?
117
+
118
+ commit_message = "rralph: completed task and updated artifacts [cycle #{@cycle_count}]"
119
+ sha = @git.commit_changes(commit_message)
120
+
121
+ if sha
122
+ log(" Git commit: #{sha}")
123
+ end
124
+
125
+ true
126
+ end
127
+
128
+ def handle_failure
129
+ if @failure_count >= @max_failures
130
+ log("Max failures reached (#{@failure_count}). Stopping to avoid infinite loops. Review learnings.md and todo.md.")
131
+ exit 1
132
+ end
133
+ true
134
+ end
135
+
136
+ def check_final_state
137
+ if @failure_count >= @max_failures
138
+ exit 1
139
+ elsif !@parser.has_pending_tasks?
140
+ log("All tasks completed! Well done!")
141
+ exit 0
142
+ end
143
+ end
144
+
145
+ def execute_ai_command(prompt)
146
+ require "tempfile"
147
+
148
+ # Write prompt to a temporary file
149
+ prompt_file = Tempfile.new(["rralph_prompt", ".txt"])
150
+ prompt_file.write(prompt)
151
+ prompt_file.close
152
+
153
+ # Read response from a temporary file
154
+ response_file = Tempfile.new(["rralph_response", ".txt"])
155
+ response_file.close
156
+
157
+ begin
158
+ # Use bash -c with proper stdin redirection
159
+ cmd = "bash -c #{@ai_command.shellescape} < #{prompt_file.path} > #{response_file.path} 2>&1"
160
+ log(" Executing: #{@ai_command}")
161
+ system(cmd)
162
+
163
+ log(" Command exit status: #{$?.exitstatus}")
164
+
165
+ response = File.read(response_file.path)
166
+ response.strip.empty? ? nil : response
167
+ ensure
168
+ prompt_file.unlink
169
+ response_file.unlink
170
+ end
171
+ rescue Errno::ENOENT => e
172
+ log("Error: AI command '#{@ai_command}' not found: #{e.message}")
173
+ nil
174
+ rescue => e
175
+ log("Error executing AI command: #{e.message}")
176
+ nil
177
+ end
178
+
179
+ def save_ai_log(prompt, response)
180
+ logs_dir = "logs"
181
+ Dir.mkdir(logs_dir) unless Dir.exist?(logs_dir)
182
+
183
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
184
+ filename = "#{logs_dir}/cycle_#{@cycle_count}_#{timestamp}.md"
185
+
186
+ # Extract just the current task for the header
187
+ task_match = prompt.match(/YOUR CURRENT TASK.*?>\s*(.+?)\n/)
188
+ task_text = task_match ? task_match[1].strip : "Unknown"
189
+
190
+ content = <<~LOG
191
+ # Cycle #{@cycle_count} - #{timestamp}
192
+
193
+ ## Current Task
194
+ #{task_text}
195
+
196
+ ## AI Response
197
+
198
+ #{response || "(no response)"}
199
+ LOG
200
+
201
+ File.write(filename, content)
202
+ log(" Log saved: #{filename}")
203
+ end
204
+
205
+ def log(message)
206
+ $stderr.puts(message)
207
+ end
208
+
209
+ def todo_empty_or_missing?
210
+ return true unless File.exist?(@todo_path)
211
+
212
+ content = File.read(@todo_path)
213
+ content.strip.empty? || !content.match?(/^- \[ \]/m)
214
+ end
215
+
216
+ def generate_todo_from_plan
217
+ prompt = <<~PROMPT
218
+ Based on the following plan, generate a todo list with actionable tasks.
219
+ Format each task as a markdown checkbox like: - [ ] Task description
220
+ Keep tasks specific and actionable.
221
+
222
+ --- plan.md ---
223
+ #{@parser.plan_content}
224
+ PROMPT
225
+
226
+ response = execute_ai_command(prompt)
227
+
228
+ if response
229
+ todo_items = response.scan(/^- \[ \] .+$/).uniq
230
+
231
+ if todo_items.any?
232
+ todo_content = "# Todo List\n\n" + todo_items.join("\n") + "\n"
233
+
234
+ File.write(@todo_path, todo_content)
235
+
236
+ commit_message = "rralph: generated todo from plan.md"
237
+ sha = @git.commit_changes(commit_message)
238
+
239
+ if sha
240
+ log("✅ Generated #{todo_items.size} tasks. Git commit: #{sha}")
241
+ end
242
+ else
243
+ log("❌ Could not parse tasks from AI response")
244
+ end
245
+ else
246
+ log("❌ AI command failed when generating todo")
247
+ end
248
+ end
249
+ end
250
+ end
data/lib/rralph.rb ADDED
@@ -0,0 +1,14 @@
1
+ module Rralph
2
+ VERSION = "0.1.0"
3
+
4
+ class Error < StandardError; end
5
+ class FileNotFound < Error; end
6
+ class GitError < Error; end
7
+ class AICommandError < Error; end
8
+ end
9
+
10
+ require_relative "rralph/parser"
11
+ require_relative "rralph/file_updater"
12
+ require_relative "rralph/git"
13
+ require_relative "rralph/runner"
14
+ require_relative "rralph/cli"
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rralph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - rralph
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.13'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.13'
54
+ - !ruby/object:Gem::Dependency
55
+ name: solargraph
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.58'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.58'
68
+ description: rralph automates an iterative, AI-assisted development workflow by reading
69
+ plan.md, learnings.md, and todo.md, then orchestrating AI tool invocations to complete
70
+ tasks.
71
+ email:
72
+ - david@joynetiks.com
73
+ executables:
74
+ - rralph
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - LICENSE
79
+ - README.md
80
+ - bin/rralph
81
+ - lib/rralph.rb
82
+ - lib/rralph/cli.rb
83
+ - lib/rralph/file_updater.rb
84
+ - lib/rralph/git.rb
85
+ - lib/rralph/parser.rb
86
+ - lib/rralph/runner.rb
87
+ homepage: https://github.com/pcboy/rralph
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ rubygems_mfa_required: 'true'
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 3.4.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.7.2
107
+ specification_version: 4
108
+ summary: A self-improving task orchestrator for AI-assisted development
109
+ test_files: []