rralph 0.1.3 → 0.2.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: 9fee5ec363c3f2bef533634cc0959815995c58ab6427496d0a14c381371a6ced
4
- data.tar.gz: c505d1dbe57ba55bbd905130713de18927fa54528a24609baf253a39a8ac7823
3
+ metadata.gz: ea4f06957671b88207938c10e6bb9a00e0a7524cea7d679b950dde54d4c99c85
4
+ data.tar.gz: d3182ee683101ee606d781093a6aba536dd25c811513520464146f60a93064bf
5
5
  SHA512:
6
- metadata.gz: efea6825e59d661ff2e9023a4cbab82a6d10b0f713e5a63bfd070ab8ae12dff087091485b5b2ecfbd95d16b830278a0f153d59a01ccebfa763ed3b75d699103d
7
- data.tar.gz: c1fdb135c8599692a362c16832ce68096d8ca064098dcd17fae1d6d227a7c2eccecd40ac8de42944898ce71b2e1ef2d8d12c7487eedacaa52521f1b58b1d8dda
6
+ metadata.gz: 2bf09138e994351d3c374ce4f1a11505ad12138138227cb7d28774b2f49ca0a257a2f0d33f1e2f2730d185284a632545fd86336d54d07dd33a4dfc06095373ff
7
+ data.tar.gz: dc175e3a19ce97b1f8fd43f1420a7e556cd99da7c7e92ae7fc2ef4fd5e390341024fca79e3eb1a9d20a6e37a2cee7ea5b772416e2fd10ec0a1d8bcf00b1c85a0
data/README.md CHANGED
@@ -111,6 +111,8 @@ Options:
111
111
  # Default: todo.md
112
112
  -s, [--skip-commit], [--no-skip-commit] # Skip git commits between tasks
113
113
  # Default: false
114
+ -v, [--verbose], [--no-verbose] # Enable verbose logging with AI thinking and real-time output
115
+ # Default: false
114
116
  ```
115
117
 
116
118
  ### Examples
@@ -172,14 +174,39 @@ Learnings: 6 lines
172
174
 
173
175
  ### Logging
174
176
 
175
- Human-readable logs are output to stderr:
177
+ By default, `rralph` outputs concise progress logs to stderr:
178
+
179
+ ```
180
+ Starting rralph with max_failures=3, ai_command='qwen-code -y -s'
181
+ Cycle 1: Processing task: Create odd_even.sh file with bash shebang
182
+ Executing AI command...
183
+ Completed in 2341ms (13407 tokens)
184
+ [Cycle 1] Task completed. 0 failures. Git commit: abc123
185
+ Log saved: logs/cycle_1_20260302_145738.md
186
+ ```
187
+
188
+ Use `--verbose` mode for detailed real-time logging including AI thinking:
189
+
190
+ ```bash
191
+ rralph start --verbose
192
+ ```
193
+
194
+ Verbose output shows:
195
+ - Thinking: AI's thought process as it thinks
196
+ - Real-time text output from the AI
197
+ - Completion metrics (duration, token usage)
198
+
199
+ Example verbose output:
176
200
 
177
201
  ```
178
- [Cycle 4] Task completed. 0 failures. Git commit: abc123
179
- [Cycle 5] FAILURE detected. Failures: 2/3
202
+ Executing AI command...
203
+ Thinking: The user wants me to create a bash script for checking even/odd numbers
204
+ Thinking: I need to start with the basic file structure and shebang
205
+ I'll create the odd_even.sh file with a proper bash shebang.
206
+ Completed in 1847ms (8234 tokens)
180
207
  ```
181
208
 
182
- In `--watch` mode, AI responses are saved to `logs/` for audit trail.
209
+ In `--watch` mode, full AI responses are saved to `logs/` for audit trail.
183
210
 
184
211
  ## License
185
212
 
data/bin/rralph CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require_relative "../lib/rralph"
3
+ require_relative '../lib/rralph'
4
4
 
5
5
  Rralph::CLI.start(ARGV)
data/lib/rralph/cli.rb CHANGED
@@ -1,44 +1,49 @@
1
- require "thor"
2
- require_relative "runner"
1
+ require 'thor'
2
+ require_relative 'runner'
3
3
 
4
4
  module Rralph
5
5
  class CLI < Thor
6
- desc "start", "Run the rralph orchestrator"
6
+ desc 'start', 'Run the rralph orchestrator'
7
7
  method_option :max_failures,
8
8
  type: :numeric,
9
9
  default: 3,
10
- aliases: "-m",
11
- desc: "Maximum allowed failures before stopping"
10
+ aliases: '-m',
11
+ desc: 'Maximum allowed failures before stopping'
12
12
  method_option :ai_command,
13
13
  type: :string,
14
- default: "qwen-code -y -s",
15
- aliases: "-a",
16
- desc: "AI command to invoke"
14
+ default: 'qwen-code -y -s',
15
+ aliases: '-a',
16
+ desc: 'AI command to invoke'
17
17
  method_option :watch,
18
18
  type: :boolean,
19
19
  default: false,
20
- aliases: "-w",
21
- desc: "Run in continuous loop until completion or max failures"
20
+ aliases: '-w',
21
+ desc: 'Run in continuous loop until completion or max failures'
22
22
  method_option :plan_path,
23
23
  type: :string,
24
- default: "plan.md",
25
- aliases: "-p",
26
- desc: "Path to plan.md file"
24
+ default: 'plan.md',
25
+ aliases: '-p',
26
+ desc: 'Path to plan.md file'
27
27
  method_option :learnings_path,
28
28
  type: :string,
29
- default: "learnings.md",
30
- aliases: "-l",
31
- desc: "Path to learnings.md file"
29
+ default: 'learnings.md',
30
+ aliases: '-l',
31
+ desc: 'Path to learnings.md file'
32
32
  method_option :todo_path,
33
33
  type: :string,
34
- default: "todo.md",
35
- aliases: "-t",
36
- desc: "Path to todo.md file"
34
+ default: 'todo.md',
35
+ aliases: '-t',
36
+ desc: 'Path to todo.md file'
37
37
  method_option :skip_commit,
38
38
  type: :boolean,
39
39
  default: false,
40
- aliases: "-s",
41
- desc: "Skip git commits between tasks"
40
+ aliases: '-s',
41
+ desc: 'Skip git commits between tasks'
42
+ method_option :verbose,
43
+ type: :boolean,
44
+ default: false,
45
+ aliases: '-v',
46
+ desc: 'Enable verbose logging with AI thinking and real-time output'
42
47
 
43
48
  def start
44
49
  runner = Runner.new(
@@ -48,41 +53,42 @@ module Rralph
48
53
  plan_path: options[:plan_path],
49
54
  learnings_path: options[:learnings_path],
50
55
  todo_path: options[:todo_path],
51
- skip_commit: options[:skip_commit]
56
+ skip_commit: options[:skip_commit],
57
+ verbose: options[:verbose]
52
58
  )
53
59
 
54
60
  runner.run
55
61
  rescue Rralph::FileNotFound => e
56
- $stderr.puts "Error: #{e.message}"
57
- $stderr.puts "Please ensure plan.md, learnings.md, and todo.md exist in the current directory."
62
+ warn "Error: #{e.message}"
63
+ warn 'Please ensure plan.md, learnings.md, and todo.md exist in the current directory.'
58
64
  exit 1
59
65
  rescue Rralph::GitError => e
60
- $stderr.puts "Git Error: #{e.message}"
66
+ warn "Git Error: #{e.message}"
61
67
  exit 1
62
- rescue => e
63
- $stderr.puts "Unexpected error: #{e.message}"
64
- $stderr.puts e.backtrace.first(5)
68
+ rescue StandardError => e
69
+ warn "Unexpected error: #{e.message}"
70
+ warn e.backtrace.first(5)
65
71
  exit 1
66
72
  end
67
73
 
68
- desc "version", "Show rralph version"
74
+ desc 'version', 'Show rralph version'
69
75
  def version
70
76
  puts "rralph v#{Rralph::VERSION}"
71
77
  end
72
78
 
73
- desc "stats", "Show progress statistics"
79
+ desc 'stats', 'Show progress statistics'
74
80
  method_option :plan_path,
75
81
  type: :string,
76
- default: "plan.md",
77
- aliases: "-p"
82
+ default: 'plan.md',
83
+ aliases: '-p'
78
84
  method_option :learnings_path,
79
85
  type: :string,
80
- default: "learnings.md",
81
- aliases: "-l"
86
+ default: 'learnings.md',
87
+ aliases: '-l'
82
88
  method_option :todo_path,
83
89
  type: :string,
84
- default: "todo.md",
85
- aliases: "-t"
90
+ default: 'todo.md',
91
+ aliases: '-t'
86
92
 
87
93
  def stats
88
94
  parser = Parser.new(
@@ -101,7 +107,7 @@ module Rralph
101
107
  puts "Pending: #{pending.size}"
102
108
  puts "Learnings: #{parser.learnings_content.lines.size} lines"
103
109
  rescue Rralph::FileNotFound => e
104
- $stderr.puts "Error: #{e.message}"
110
+ warn "Error: #{e.message}"
105
111
  exit 1
106
112
  end
107
113
 
@@ -1,6 +1,6 @@
1
1
  module Rralph
2
2
  class FileUpdater
3
- def initialize(todo_path: "todo.md", learnings_path: "learnings.md")
3
+ def initialize(todo_path: 'todo.md', learnings_path: 'learnings.md')
4
4
  @todo_path = todo_path
5
5
  @learnings_path = learnings_path
6
6
  end
@@ -10,17 +10,17 @@ module Rralph
10
10
  lines = content.lines
11
11
 
12
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
13
+ return unless line
14
+
15
+ updated_line = line.gsub(/^([-*]) \[ \]/, '\1 [x]')
16
+ lines[task_index] = updated_line
17
+ File.write(@todo_path, lines.join)
18
18
  end
19
19
 
20
20
  def append_learnings(new_learnings)
21
21
  return if new_learnings.empty?
22
22
 
23
- existing_content = File.exist?(@learnings_path) ? File.read(@learnings_path) : ""
23
+ existing_content = File.exist?(@learnings_path) ? File.read(@learnings_path) : ''
24
24
  existing_learnings = existing_content.lines.map(&:strip).reject(&:empty?)
25
25
 
26
26
  unique_learnings = new_learnings.reject do |learning|
@@ -29,7 +29,7 @@ module Rralph
29
29
 
30
30
  return if unique_learnings.empty?
31
31
 
32
- timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
32
+ timestamp = Time.now.iso8601
33
33
  new_section = "\n\n## Learnings - #{timestamp}\n\n"
34
34
  unique_learnings.each do |learning|
35
35
  new_section += "- #{learning}\n"
data/lib/rralph/git.rb CHANGED
@@ -1,28 +1,22 @@
1
- require "shellwords"
1
+ require 'shellwords'
2
2
 
3
3
  module Rralph
4
4
  class Git
5
- def initialize
6
- end
5
+ def initialize; end
7
6
 
8
7
  def in_git_repo?
9
- result = `git rev-parse --git-dir 2>/dev/null`
8
+ `git rev-parse --git-dir 2>/dev/null`
10
9
  $?.success?
11
10
  end
12
11
 
13
12
  def commit_changes(message)
14
13
  add_result = `git add . 2>&1`
15
- unless $?.success?
16
- raise GitError, "Failed to stage files: #{add_result}"
17
- end
14
+ raise GitError, "Failed to stage files: #{add_result}" unless $?.success?
18
15
 
19
16
  commit_result = `git commit -m #{message.shellescape} 2>&1`
20
- unless $?.success?
21
- raise GitError, "Failed to commit: #{commit_result}"
22
- end
17
+ raise GitError, "Failed to commit: #{commit_result}" unless $?.success?
23
18
 
24
- sha = `git rev-parse --short HEAD 2>/dev/null`.strip
25
- sha
19
+ `git rev-parse --short HEAD 2>/dev/null`.strip
26
20
  end
27
21
 
28
22
  def status
data/lib/rralph/parser.rb CHANGED
@@ -2,18 +2,18 @@ module Rralph
2
2
  class Parser
3
3
  attr_reader :plan_content, :learnings_content, :todo_content
4
4
 
5
- def initialize(plan_path: "plan.md", learnings_path: "learnings.md", todo_path: "todo.md")
5
+ def initialize(plan_path: 'plan.md', learnings_path: 'learnings.md', todo_path: 'todo.md')
6
6
  @plan_path = plan_path
7
7
  @learnings_path = learnings_path
8
8
  @todo_path = todo_path
9
9
  end
10
10
 
11
11
  def load_files
12
- raise FileNotFound, "plan.md not found" unless File.exist?(@plan_path)
12
+ raise FileNotFound, 'plan.md not found' unless File.exist?(@plan_path)
13
13
 
14
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) : ""
15
+ @learnings_content = File.exist?(@learnings_path) ? File.read(@learnings_path) : ''
16
+ @todo_content = File.exist?(@todo_path) ? File.read(@todo_path) : ''
17
17
  end
18
18
 
19
19
  def pending_tasks
@@ -21,9 +21,9 @@ module Rralph
21
21
 
22
22
  @todo_content.lines.map.with_index do |line, index|
23
23
  stripped = line.strip
24
- next unless stripped.start_with?("- [ ]") || stripped.start_with?("* [ ]")
24
+ next unless stripped.start_with?('- [ ]') || stripped.start_with?('* [ ]')
25
25
 
26
- task_text = stripped.sub(/^[-*] \[ \] /, "").strip
26
+ task_text = stripped.sub(/^[-*] \[ \] /, '').strip
27
27
  { index: index, line: line, text: task_text, raw: stripped }
28
28
  end.compact
29
29
  end
@@ -33,9 +33,9 @@ module Rralph
33
33
 
34
34
  @todo_content.lines.map.with_index do |line, index|
35
35
  stripped = line.strip
36
- next unless stripped.start_with?("- [x]") || stripped.start_with?("* [x]")
36
+ next unless stripped.start_with?('- [x]') || stripped.start_with?('* [x]')
37
37
 
38
- task_text = stripped.sub(/^[-*] \[x\] /i, "").strip
38
+ task_text = stripped.sub(/^[-*] \[x\] /i, '').strip
39
39
  { index: index, line: line, text: task_text, raw: stripped }
40
40
  end.compact
41
41
  end
@@ -48,7 +48,7 @@ module Rralph
48
48
  next unless stripped.match?(/^[-*] \[[ x]\]/i)
49
49
 
50
50
  completed = stripped.match?(/^[-*] \[x\]/i)
51
- task_text = stripped.sub(/^[-*] \[[ x]\] /i, "").strip
51
+ task_text = stripped.sub(/^[-*] \[[ x]\] /i, '').strip
52
52
  { index: index, line: line, text: task_text, raw: stripped, completed: completed }
53
53
  end.compact
54
54
  end
@@ -67,11 +67,26 @@ module Rralph
67
67
  # Match various learning patterns like:
68
68
  # "Learning: xyz", "**Learning to add:** xyz", "- Learning: xyz", etc.
69
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?
70
+ learning_pattern = /
71
+ (?:^|\s) # Start of line or whitespace
72
+ (?:\*\*)? # Optional bold markdown
73
+ (?:Learning|Insight|Note|Tip|Discovered|Found|Realized) # Learning keywords
74
+ [\s*]* # Optional whitespace or asterisks
75
+ [:\s]* # Optional colons or whitespace
76
+ (?:to\sadd)? # Optional "to add"
77
+ [:\s]* # Optional colons or whitespace
78
+ (.+?) # Capture the actual learning content
79
+ (?:\*\*)? # Optional closing bold markdown
80
+ (?:\s*$) # Optional whitespace to end of line
81
+ /ix
82
+
83
+ unless match = line.match(learning_pattern)
84
+ next
74
85
  end
86
+
87
+ learning = match[1].strip
88
+ learning = learning.gsub(/^\*\*|\*\*$/, '').strip
89
+ learnings << learning unless learning.empty?
75
90
  end
76
91
 
77
92
  # Also extract from ## Learnings sections
@@ -81,7 +96,8 @@ module Rralph
81
96
  section.lines.each do |line|
82
97
  stripped = line.strip
83
98
  next if stripped.empty?
84
- stripped = stripped.sub(/^[-*]\s*/, "")
99
+
100
+ stripped = stripped.sub(/^[-*]\s*/, '')
85
101
  learnings << stripped unless stripped.empty?
86
102
  end
87
103
  end
@@ -92,10 +108,10 @@ module Rralph
92
108
  def build_prompt(current_task: nil)
93
109
  <<~PROMPT
94
110
  You are in an iterative development loop. There is a todo list with tasks.
95
-
111
+
96
112
  YOUR CURRENT TASK (the first unchecked item in todo.md):
97
113
  > #{current_task}
98
-
114
+
99
115
  INSTRUCTIONS - Follow these steps in order:
100
116
  1. Implement ONLY the task shown above
101
117
  2. Write a unit test for it
@@ -104,14 +120,14 @@ module Rralph
104
120
  - "DONE" if the task is complete and test passes
105
121
  - "FAILURE" if the test fails after your best effort
106
122
  5. Optionally add learnings as: "Learning: <insight>"
107
-
123
+
108
124
  IMPORTANT RULES:
109
125
  - Work on ONE task only - the one shown above
110
126
  - Do NOT implement other tasks from the todo list
111
127
  - Do NOT mark tasks as done yourself
112
128
  - After you respond "DONE", the system will mark this task complete
113
129
  - Then you will receive the next task
114
-
130
+
115
131
  --- plan.md (context) ---
116
132
  #{@plan_content}
117
133
 
data/lib/rralph/runner.rb CHANGED
@@ -1,15 +1,19 @@
1
+ require 'json'
2
+ require 'open3'
3
+ require 'amazing_print'
1
4
  module Rralph
2
5
  class Runner
3
6
  attr_reader :cycle_count, :failure_count, :max_failures
4
7
 
5
8
  def initialize(
6
9
  max_failures: 3,
7
- ai_command: "qwen-code -y -s",
10
+ ai_command: 'qwen-code -y -s',
8
11
  watch: false,
9
- plan_path: "plan.md",
10
- learnings_path: "learnings.md",
11
- todo_path: "todo.md",
12
- skip_commit: false
12
+ plan_path: 'plan.md',
13
+ learnings_path: 'learnings.md',
14
+ todo_path: 'todo.md',
15
+ skip_commit: false,
16
+ verbose: false
13
17
  )
14
18
  @max_failures = max_failures
15
19
  @ai_command = ai_command
@@ -18,6 +22,7 @@ module Rralph
18
22
  @learnings_path = learnings_path
19
23
  @todo_path = todo_path
20
24
  @skip_commit = skip_commit
25
+ @verbose = verbose
21
26
 
22
27
  @cycle_count = 0
23
28
  @failure_count = 0
@@ -37,28 +42,24 @@ module Rralph
37
42
  def run
38
43
  log("Starting rralph with max_failures=#{@max_failures}, ai_command='#{@ai_command}'")
39
44
 
40
- unless @git.in_git_repo?
41
- raise GitError, "Not in a git repository. Please initialize git first."
42
- end
45
+ raise GitError, 'Not in a git repository. Please initialize git first.' unless @git.in_git_repo?
43
46
 
44
47
  @parser.load_files
45
48
 
46
49
  if todo_empty_or_missing?
47
- log("todo.md is empty or missing. Generating todo list from plan...")
50
+ log('todo.md is empty or missing. Generating todo list from plan...')
48
51
  generate_todo_from_plan
49
52
  return true
50
53
  end
51
54
 
52
55
  unless @parser.has_pending_tasks?
53
- log("All tasks completed! Well done!")
56
+ log('All tasks completed! Well done!')
54
57
  return true
55
58
  end
56
59
 
57
- if @watch
58
- run_watch_loop
59
- else
60
- run_single_cycle
61
- end
60
+ return run_watch_loop if @watch
61
+
62
+ run_single_cycle
62
63
  end
63
64
 
64
65
  private
@@ -69,8 +70,6 @@ module Rralph
69
70
  break unless success
70
71
  break unless @parser.has_pending_tasks?
71
72
  break if @failure_count >= @max_failures
72
-
73
- sleep 1
74
73
  end
75
74
 
76
75
  check_final_state
@@ -82,7 +81,7 @@ module Rralph
82
81
  @parser.load_files
83
82
 
84
83
  unless @parser.has_pending_tasks?
85
- log("All tasks completed! Well done!")
84
+ log('All tasks completed! Well done!')
86
85
  return false
87
86
  end
88
87
 
@@ -118,14 +117,12 @@ module Rralph
118
117
  @file_updater.append_learnings(new_learnings) if new_learnings.any?
119
118
 
120
119
  if @skip_commit
121
- log("⏭️ [Cycle #{@cycle_count}] Skipping commit (skip_commit enabled)")
120
+ log("⏭️ [Cycle #{@cycle_count}] Skipping commit (skip_commit enabled)")
122
121
  else
123
122
  commit_message = "rralph: #{current_task[:text]} [cycle #{@cycle_count}]"
124
123
  sha = @git.commit_changes(commit_message)
125
124
 
126
- if sha
127
- log(" Git commit: #{sha}")
128
- end
125
+ log(" Git commit: #{sha}") if sha
129
126
  end
130
127
 
131
128
  true
@@ -143,55 +140,99 @@ module Rralph
143
140
  if @failure_count >= @max_failures
144
141
  exit 1
145
142
  elsif !@parser.has_pending_tasks?
146
- log("All tasks completed! Well done!")
143
+ log('All tasks completed! Well done!')
147
144
  exit 0
148
145
  end
149
146
  end
150
147
 
151
148
  def execute_ai_command(prompt)
152
- require "tempfile"
149
+ full_response = ''
153
150
 
154
- # Write prompt to a temporary file
155
- prompt_file = Tempfile.new(["rralph_prompt", ".txt"])
156
- prompt_file.write(prompt)
157
- prompt_file.close
151
+ begin
152
+ log('Executing AI command...')
153
+
154
+ # Use stream-json mode for real-time logging
155
+ cmd = "#{@ai_command} -y -s -o stream-json"
156
+
157
+ Open3.popen3(cmd) do |stdin, stdout, _stderr, wait_thr|
158
+ # Write prompt to stdin and close
159
+ stdin.write(prompt)
160
+ stdin.close
161
+
162
+ # Process stdout line by line for real-time streaming
163
+ stdout.each_line do |line|
164
+ event = JSON.parse(line)
165
+
166
+ case event['type']
167
+ when 'assistant'
168
+ content = event.dig('message', 'content')
169
+ content&.each do |item|
170
+ case item['type']
171
+ when 'thinking'
172
+ log("[Thinking] #{item['thinking']}") if @verbose
173
+ when 'text'
174
+ text = item['text']
175
+ full_response += text
176
+ log("[text] #{text}") if @verbose
177
+ when 'tool_use'
178
+ tool_name = item['name']
179
+ tool_input = item['input']
180
+ log("[tool_use] #{tool_name}: #{tool_input.to_json}") if @verbose
181
+ end
182
+ end
183
+ when 'user'
184
+ if @verbose
185
+ content = event.dig('message', 'content')
186
+ content&.each do |item|
187
+ if item['type'] == 'tool_result'
188
+ tool_name = item.dig('tool_use_id')&.split('_')&.first || 'tool'
189
+ result = item['content']
190
+ log("[#{tool_name}] #{result}")
191
+ elsif item['type'] == 'text'
192
+ log("[text] #{item['text']}")
193
+ end
194
+ end
195
+ end
196
+ when 'result'
197
+ duration = event['duration_ms']
198
+ tokens = event.dig('usage', 'total_tokens')
199
+ is_error = event['is_error']
200
+ if is_error
201
+ log('AI command failed')
202
+ else
203
+ log("Completed in #{duration}ms (#{tokens} tokens)")
204
+ end
205
+ else
206
+ log(" [event: #{event['type']}]") if @verbose
207
+ end
208
+ rescue JSON::ParserError
209
+ # Skip non-JSON lines
210
+ log(" #{line.chomp}") if @verbose
211
+ end
158
212
 
159
- # Read response from a temporary file
160
- response_file = Tempfile.new(["rralph_response", ".txt"])
161
- response_file.close
213
+ log(" Command exit status: #{wait_thr.value.exitstatus}")
214
+ end
162
215
 
163
- begin
164
- # Use bash -c with proper stdin redirection
165
- cmd = "bash -c #{@ai_command.shellescape} < #{prompt_file.path} > #{response_file.path} 2>&1"
166
- log(" Executing: #{@ai_command}")
167
- system(cmd)
168
-
169
- log(" Command exit status: #{$?.exitstatus}")
170
-
171
- response = File.read(response_file.path)
172
- response.strip.empty? ? nil : response
173
- ensure
174
- prompt_file.unlink
175
- response_file.unlink
216
+ full_response.strip.empty? ? nil : full_response
217
+ rescue Errno::ENOENT => e
218
+ log("Error: AI command '#{@ai_command}' not found: #{e.message}")
219
+ nil
220
+ rescue StandardError => e
221
+ log("Error executing AI command: #{e.message}")
222
+ nil
176
223
  end
177
- rescue Errno::ENOENT => e
178
- log("Error: AI command '#{@ai_command}' not found: #{e.message}")
179
- nil
180
- rescue => e
181
- log("Error executing AI command: #{e.message}")
182
- nil
183
224
  end
184
225
 
185
226
  def save_ai_log(prompt, response)
186
- logs_dir = "logs"
227
+ logs_dir = 'logs'
187
228
  Dir.mkdir(logs_dir) unless Dir.exist?(logs_dir)
188
229
 
189
- timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
230
+ timestamp = Time.now.iso8601
190
231
  filename = "#{logs_dir}/cycle_#{@cycle_count}_#{timestamp}.md"
191
232
 
192
233
  # Extract just the current task for the header
193
234
  task_match = prompt.match(/YOUR CURRENT TASK.*?\n>\s*(.+?)\n/)
194
- task_text = task_match ? task_match[1].strip : "Unknown"
235
+ task_text = task_match ? task_match[1].strip : 'Unknown'
195
236
 
196
237
  content = <<~LOG
197
238
  # Cycle #{@cycle_count} - #{timestamp}
@@ -201,7 +242,7 @@ module Rralph
201
242
 
202
243
  ## AI Response
203
244
 
204
- #{response || "(no response)"}
245
+ #{response || '(no response)'}
205
246
  LOG
206
247
 
207
248
  File.write(filename, content)
@@ -209,18 +250,31 @@ module Rralph
209
250
  end
210
251
 
211
252
  def log(message)
212
- $stderr.puts(message)
253
+ warn(message)
213
254
  end
214
255
 
215
256
  def todo_empty_or_missing?
216
257
  return true unless File.exist?(@todo_path)
217
258
 
218
259
  content = File.read(@todo_path)
219
- content.strip.empty? || !content.match?(/^- \[ \]/m)
260
+ content.strip.empty?
220
261
  end
221
262
 
222
263
  def generate_todo_from_plan
223
- prompt = <<~PROMPT
264
+ prompt = build_todo_generation_prompt
265
+ response = execute_ai_command(prompt)
266
+
267
+ return log('❌ AI command failed when generating todo') unless response
268
+
269
+ todo_items = extract_todo_items(response)
270
+ return log('❌ Could not parse tasks from AI response') if todo_items.empty?
271
+
272
+ save_todo_file(todo_items)
273
+ handle_todo_commit(todo_items.size)
274
+ end
275
+
276
+ def build_todo_generation_prompt
277
+ <<~PROMPT
224
278
  Based on the following plan, generate a todo list with actionable tasks.
225
279
  Format each task as a markdown checkbox like: - [ ] Task description
226
280
  Keep tasks specific and actionable.
@@ -228,32 +282,29 @@ module Rralph
228
282
  --- plan.md ---
229
283
  #{@parser.plan_content}
230
284
  PROMPT
285
+ end
231
286
 
232
- response = execute_ai_command(prompt)
233
-
234
- if response
235
- todo_items = response.scan(/^- \[ \] .+$/).uniq
287
+ def extract_todo_items(response)
288
+ response.scan(/^- \[ \] .+$/).uniq
289
+ end
236
290
 
237
- if todo_items.any?
238
- todo_content = "# Todo List\n\n" + todo_items.join("\n") + "\n"
291
+ def save_todo_file(todo_items)
292
+ todo_content = <<~TODO
293
+ # Todo List
239
294
 
240
- File.write(@todo_path, todo_content)
295
+ #{todo_items.join("\n")}
296
+ TODO
241
297
 
242
- if @skip_commit
243
- log("⏭️ Generated #{todo_items.size} tasks (skip_commit enabled, no commit)")
244
- else
245
- commit_message = "rralph: generated todo from plan.md"
246
- sha = @git.commit_changes(commit_message)
298
+ File.write(@todo_path, todo_content)
299
+ end
247
300
 
248
- if sha
249
- log("✅ Generated #{todo_items.size} tasks. Git commit: #{sha}")
250
- end
251
- end
252
- else
253
- log("❌ Could not parse tasks from AI response")
254
- end
301
+ def handle_todo_commit(task_count)
302
+ if @skip_commit
303
+ log("⏭️ Generated #{task_count} tasks (skip_commit enabled, no commit)")
255
304
  else
256
- log("❌ AI command failed when generating todo")
305
+ commit_message = 'rralph: generated todo from plan.md'
306
+ sha = @git.commit_changes(commit_message)
307
+ log("✅ Generated #{task_count} tasks. Git commit: #{sha}") if sha
257
308
  end
258
309
  end
259
310
  end
data/lib/rralph.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Rralph
2
- VERSION = "0.1.3"
2
+ VERSION = '0.2.0'
3
3
 
4
4
  class Error < StandardError; end
5
5
  class FileNotFound < Error; end
@@ -7,8 +7,8 @@ module Rralph
7
7
  class AICommandError < Error; end
8
8
  end
9
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"
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 CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rralph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - rralph