code_healer 0.1.26 → 0.1.33

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: 20c5c060c65cfee68cf65f2478e6335b1927f7e810fdcf6dd1c6796e29b49f65
4
- data.tar.gz: 38aad1427647fe628c1a0aa2824fe09b206d6ac752ef6b1407a41ddd15560383
3
+ metadata.gz: 8c3a36c401bef8aaddaf43a1b582bd0d398dc3f6d02e1618bc913327110eff02
4
+ data.tar.gz: 55375ba10a91f2c3ec1bea92c77034d1d5e7044c0a9bb6ca1f0a260531d03732
5
5
  SHA512:
6
- metadata.gz: eb2f391d38dfb0e444ff60e66d92625409f09c8389705a08c3ca3783f9aa56ae1d10dffc36109472cf2158c1e44b3df1eaba6100263f57d7d82bec713d58bbe6
7
- data.tar.gz: 7864c4f255da0a1682d8c5787009998fe0e554a29fadbaf486f84d3c7664cdb12596381d8affa303110dc68a7b42d83c00f10da94c7f5f7368bc8bed88d13189
6
+ metadata.gz: 34d758a6ec20dda236e9a7631d2520fb4e6cd8a08c5d5dfdb758b15238a8453003d7c8ccb677a2d3f813e5597e09c4808fc0ffbbb402f010a8ebcc6b90508ed1
7
+ data.tar.gz: 2a67d461d89c20515538510106a0eb544a8294954d61b152e95a6d92722553c062b9c5ce05a72a974579c96f84206d79e7fdd57a4317ad1ec3162c40a4e31cb6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## [0.1.33] - 2025-09-15
2
+
3
+ ### Fixed
4
+ - Syntax error in `HealingWorkspaceManager#test_fixes_in_workspace` unmatched `end` corrected.
5
+
6
+ ## [0.1.32] - 2025-09-15
7
+
8
+ ### Added
9
+ - Targeted RSpec test-fix loop after code changes in `ClaudeCodeEvolutionHandler`:
10
+ - Runs specs for files related to recent modifications
11
+ - Parses failures and re-invokes Claude with failure summary
12
+ - Iterates up to `test_fix.max_iterations` (default 2)
13
+ - Configuration: `test_fix.max_iterations` with sensible default in `ConfigManager` and `setup.rb`.
14
+
15
+ ### Changed
16
+ - Removed demo mode code and comments across gem: prompts, setup script, logger phrasing, and workspace manager.
17
+ - Switched class targeting to excluded-classes-only model; removed `allowed_classes` usage in config managers and setup generation.
18
+
19
+ ### Notes
20
+ - Backwards compatible; no breaking API changes. Configure `test_fix.max_iterations` in `config/code_healer.yml` to tune retries.
21
+
1
22
  # Changelog
2
23
 
3
24
  All notable changes to this project will be documented in this file.
@@ -12,31 +12,25 @@ module CodeHealer
12
12
  PresentationLogger.detail("File: #{file_path}")
13
13
 
14
14
  begin
15
- # Build concise, demo-optimized prompt (no repo-wide scan, no tests)
15
+ # Build concise prompt
16
16
  prompt = BusinessContextManager.build_claude_code_prompt(
17
17
  error, class_name, method_name, file_path
18
18
  )
19
+
19
20
  prompt << "\n\nStrict instructions:" \
20
21
  "\n- Do NOT scan the entire codebase." \
21
22
  "\n- Work only with the provided file/method context and backtrace." \
22
23
  "\n- Return a unified diff (no prose)." \
23
- "\n- Keep changes minimal and safe." \
24
- "\n- Do NOT create or run tests." if CodeHealer::ConfigManager.demo_mode?
24
+ "\n- Keep changes minimal and safe."
25
25
 
26
26
  # Execute Claude Code command
27
27
  success = execute_claude_code_fix(prompt, class_name, method_name)
28
28
 
29
29
  if success
30
30
  PresentationLogger.success("Claude run completed")
31
- # Reload modified files
32
31
  reload_modified_files
33
-
34
- # 🚀 Trigger Git operations (commit, push, PR creation)
35
- # Note: Git operations are now handled by the isolated workspace manager
36
- # to prevent duplication and ensure proper isolation
37
- PresentationLogger.info("Git operations handled by workspace manager")
38
-
39
- return true
32
+ # Run targeted RSpec and iterate on failures
33
+ return run_tests_and_iterate_fixes(class_name, method_name)
40
34
  else
41
35
  PresentationLogger.error("Claude run failed")
42
36
  return false
@@ -68,6 +62,10 @@ module CodeHealer
68
62
  PresentationLogger.success("Response received from Claude")
69
63
  PresentationLogger.detail(stdout)
70
64
 
65
+
66
+
67
+ # Business context references are intentionally not logged
68
+
71
69
  # Check if Claude Code is asking for permission
72
70
  if stdout.include?("permission") || stdout.include?("grant") || stdout.include?("edit")
73
71
  PresentationLogger.warn("Claude requested edit permissions. Ensure permissions are granted.")
@@ -114,25 +112,118 @@ module CodeHealer
114
112
  # Replace placeholder
115
113
  command = command_template.gsub('{prompt}', escaped_prompt)
116
114
 
117
- # Add demo mode specific instructions
118
- if CodeHealer::ConfigManager.demo_mode?
119
- command += " --append-system-prompt 'DEMO MODE: Focus on quick fixes, skip tests, limit file changes to #{config['max_file_changes'] || 3} files maximum'"
120
- else
121
- if config['include_tests']
122
- command += " --append-system-prompt 'Include tests when fixing the code'"
123
- end
124
-
125
- if config['max_file_changes']
126
- command += " --append-system-prompt 'Limit changes to #{config['max_file_changes']} files maximum'"
127
- end
115
+ # Add limits and testing hints
116
+ if config['include_tests']
117
+ command += " --append-system-prompt 'Include tests when fixing the code'"
118
+ end
119
+
120
+ if config['max_file_changes']
121
+ command += " --append-system-prompt 'Limit changes to #{config['max_file_changes']} files maximum'"
128
122
  end
129
123
 
130
- # Add business context instructions
131
- command += " --append-system-prompt 'CRITICAL: Before fixing any code, you MUST use the Atlassian MCP tools to search Confluence for business context. Search for documentation about the class and method you are fixing. Only proceed with the fix after reviewing the business requirements from Confluence. If no documentation is found, note this in your response.'"
132
-
124
+ # Add business context instructions and require a delimited summary we can parse (Confluence only)
125
+ # command += " --append-system-prompt 'CRITICAL: Before fixing any code, use the Atlassian MCP tools to fetch business context from Confluence ONLY. Summarize the relevant findings in a concise, bullet list between the markers <<CONTEXT_START>> and <<CONTEXT_END>>. Include Confluence page titles and links, and key rules. Keep to <=5 bullets. Then proceed with the fix.'"
126
+
127
+ # Explicit Confluence page fetch (env override with default fallback)
128
+ explicit_confluence_page_id = (ENV['CONFLUENCE_PAGE_ID'] || '4949770295').to_s.strip
129
+ unless explicit_confluence_page_id.empty?
130
+ command += " --append-system-prompt 'CRITICAL: Before fixing any code, Explicitly fetch Confluence page ID #{explicit_confluence_page_id} using Atlassian MCP (mcp__atlassian), extract applicable business logic/rules, and APPLY those rules in the fix.'"
131
+ end
132
+
133
133
  # Return command
134
134
  command
135
135
  end
136
+
137
+ # Run targeted specs for changed files and iterate fixes up to a configured limit
138
+ def run_tests_and_iterate_fixes(class_name, method_name)
139
+ max_iters = ConfigManager.max_test_fix_iterations
140
+ it = 0
141
+ loop do
142
+ it += 1
143
+ PresentationLogger.step("RSpec run #{it}/#{max_iters}")
144
+ failures = run_targeted_rspec_for_changes
145
+ if failures.nil?
146
+ PresentationLogger.warn("No RSpec detected or no changed files with specs. Skipping test loop.")
147
+ return true
148
+ end
149
+ if failures.empty?
150
+ PresentationLogger.success("All targeted specs passed")
151
+ return true
152
+ end
153
+ PresentationLogger.warn("Failures detected (#{failures.size}). Attempting fix iteration...")
154
+ break if it >= max_iters
155
+ attempt_fix_from_failures(failures, class_name, method_name)
156
+ end
157
+ PresentationLogger.warn("Reached max test-fix iterations (#{max_iters}).")
158
+ false
159
+ end
160
+
161
+ def run_targeted_rspec_for_changes
162
+ changed = get_recently_modified_files.select { |f| f.end_with?('.rb') }
163
+ spec_files = changed.map { |f| f.sub(%r{^app/}, 'spec/').sub(/\.rb\z/, '_spec.rb') }
164
+ spec_files.select! { |s| File.exist?(s) }
165
+ return nil if spec_files.empty? || !File.exist?('spec')
166
+ cmd = ["bundle exec rspec --format documentation --no-color", spec_files.map { |s| "'#{s}'" }.join(' ')].join(' ')
167
+ stdout, stderr, status = Open3.capture3(cmd)
168
+ PresentationLogger.detail(stdout) if stdout && !stdout.empty?
169
+ PresentationLogger.warn(stderr) if stderr && !stderr.empty?
170
+ parse_rspec_failures(stdout)
171
+ rescue => e
172
+ PresentationLogger.warn("RSpec execution failed: #{e.message}")
173
+ nil
174
+ end
175
+
176
+ def parse_rspec_failures(output)
177
+ return [] unless output
178
+ failures = []
179
+ current = nil
180
+ output.each_line do |line|
181
+ if line =~ /^\s*\d+\)\s+(.*)$/
182
+ current = { title: $1.strip, details: [] }
183
+ failures << current
184
+ elsif current
185
+ current[:details] << line
186
+ end
187
+ end
188
+ failures
189
+ end
190
+
191
+ def attempt_fix_from_failures(failures, class_name, method_name)
192
+ summary = failures.map { |f| "- #{f[:title]}\n #{f[:details].first(5).join}" }.join("\n")
193
+ PresentationLogger.claude_action("Sending failure summary to Claude for iterative fix")
194
+ iterative_prompt = <<~PROMPT
195
+ The previous fix compiled, but targeted RSpec tests failed. Here is a concise failure summary:\n\n#{summary}\n\nUpdate the relevant code to make these tests pass. Return only a unified diff. Keep changes minimal and safe.
196
+ PROMPT
197
+ config = ConfigManager.claude_code_settings
198
+ command = build_claude_command(iterative_prompt, config)
199
+ stdout, stderr, status = Open3.capture3(command)
200
+ PresentationLogger.detail(stdout) if stdout && !stdout.empty?
201
+ PresentationLogger.warn(stderr) if stderr && !stderr.empty?
202
+ if status.success?
203
+ PresentationLogger.success("Iterative Claude run succeeded")
204
+ reload_modified_files
205
+ true
206
+ else
207
+ PresentationLogger.error("Iterative Claude run failed (status #{status.exitstatus})")
208
+ false
209
+ end
210
+ end
211
+
212
+ # Parse Claude Terminal output for Confluence links/titles (Confluence only)
213
+ def extract_business_context_references(text)
214
+ refs = []
215
+ return refs unless text
216
+
217
+ # Match Confluence URLs
218
+ confluence_regex = /(https?:\/\/[^\s]+confluence[^\s]+\/(display|spaces|pages)\/[^\s)"']+)/i
219
+ text.scan(confluence_regex).each do |match|
220
+ url = match[0]
221
+ title = url.split('/').last.gsub('-', ' ')[0..80]
222
+ refs << { source: 'Confluence', display: "#{title} (#{url})" }
223
+ end
224
+
225
+ refs.uniq { |r| r[:display] }
226
+ end
136
227
 
137
228
  def reload_modified_files
138
229
  PresentationLogger.step("Reloading modified files")
@@ -34,8 +34,9 @@ module CodeHealer
34
34
  config['allowed_error_types'] || []
35
35
  end
36
36
 
37
+ # Deprecated: allowed classes are no longer used. We rely solely on excluded_classes.
37
38
  def allowed_classes
38
- config['allowed_classes'] || []
39
+ []
39
40
  end
40
41
 
41
42
  def excluded_classes
@@ -45,8 +46,7 @@ module CodeHealer
45
46
  def can_evolve_class?(class_name)
46
47
  return false unless enabled?
47
48
  return false if excluded_classes.include?(class_name)
48
- return true if allowed_classes.empty?
49
- allowed_classes.include?(class_name)
49
+ true
50
50
  end
51
51
 
52
52
  def can_handle_error?(error)
@@ -82,6 +82,15 @@ module CodeHealer
82
82
  config['claude_code'] || {}
83
83
  end
84
84
 
85
+ # Test-fix iteration settings
86
+ def test_fix_settings
87
+ config['test_fix'] || {}
88
+ end
89
+
90
+ def max_test_fix_iterations
91
+ (test_fix_settings['max_iterations'] || 2).to_i
92
+ end
93
+
85
94
  def claude_persist_session?
86
95
  claude_code_settings['persist_session'] == true
87
96
  end
@@ -163,21 +172,6 @@ module CodeHealer
163
172
  config['api'] || {}
164
173
  end
165
174
 
166
- # Demo Configuration
167
- def demo_settings
168
- config['demo'] || {}
169
- end
170
-
171
- def demo_mode?
172
- demo_settings['enabled'] == true
173
- end
174
-
175
- def demo_skip_tests?
176
- demo_mode? && demo_settings['skip_tests'] != false
177
- end
178
-
179
-
180
-
181
175
  def git_settings
182
176
  config['git'] || {}
183
177
  end
@@ -337,7 +331,6 @@ module CodeHealer
337
331
  'max_evolutions_per_day' => 10,
338
332
  'auto_generate_tests' => true,
339
333
  'allowed_error_types' => ['ZeroDivisionError', 'NoMethodError', 'ArgumentError', 'TypeError'],
340
- 'allowed_classes' => ['User', 'Order', 'PaymentProcessor'],
341
334
  'excluded_classes' => ['ApplicationController', 'ApplicationRecord', 'ApplicationJob', 'ApplicationMailer'],
342
335
  'evolution_strategy' => {
343
336
  'method' => 'api',
@@ -355,6 +348,9 @@ module CodeHealer
355
348
  'spec/business_context_specs.rb'
356
349
  ]
357
350
  },
351
+ 'test_fix' => {
352
+ 'max_iterations' => 2
353
+ },
358
354
  'business_context' => {
359
355
  'enabled' => true,
360
356
  'sources' => ['docs/business_rules.md']
@@ -365,10 +361,7 @@ module CodeHealer
365
361
  'max_tokens' => 2000,
366
362
  'temperature' => 0.1
367
363
  },
368
- 'demo' => {
369
- 'enabled' => false,
370
- 'skip_tests' => true
371
- },
364
+
372
365
  'git' => {
373
366
  'auto_commit' => true,
374
367
  'auto_push' => true,
@@ -1,6 +1,7 @@
1
1
  require 'logger'
2
2
  require 'git'
3
3
  require 'octokit'
4
+ require_relative 'presentation_logger'
4
5
 
5
6
  module CodeHealer
6
7
  class Core
@@ -180,15 +181,25 @@ module CodeHealer
180
181
  end
181
182
 
182
183
  def patch_classes_for_evolution
183
- # Get allowed classes from config
184
- allowed_classes = CodeHealer::ConfigManager.allowed_classes
185
-
186
- allowed_classes.each do |class_name|
187
- begin
188
- klass = class_name.constantize
189
- patch_class_for_evolution(klass)
190
- rescue NameError => e
191
- puts "⚠️ Could not load class #{class_name}: #{e.message}"
184
+ # Patch all autoloaded classes except excluded ones
185
+ excluded = CodeHealer::ConfigManager.excluded_classes
186
+ # Attempt to iterate over loaded application classes
187
+ if defined?(Rails) && Rails.application
188
+ app_paths = [Rails.root.join('app', 'models'), Rails.root.join('app', 'controllers')]
189
+ app_paths.each do |path|
190
+ Dir.glob(File.join(path.to_s, '**', '*.rb')).each do |file|
191
+ begin
192
+ relative = file.sub(Rails.root.join('app').to_s + '/', '')
193
+ class_name = relative.gsub('.rb', '').split('/').map(&:classify).join('::')
194
+ next if class_name.nil? || class_name.empty?
195
+ next if excluded.include?(class_name)
196
+ klass = class_name.constantize rescue nil
197
+ next unless klass.is_a?(Class) || klass.is_a?(Module)
198
+ patch_class_for_evolution(klass)
199
+ rescue => e
200
+ puts "⚠️ Skipping #{file}: #{e.message}"
201
+ end
202
+ end
192
203
  end
193
204
  end
194
205
  end
@@ -229,26 +240,26 @@ module CodeHealer
229
240
  def extract_from_backtrace(backtrace)
230
241
  return [nil, nil] unless backtrace
231
242
 
232
- puts "🔍 DEBUG: Starting backtrace analysis..."
233
- puts "🔍 DEBUG: First 5 backtrace lines:"
234
- backtrace.first(5).each_with_index { |line, i| puts " #{i}: #{line}" }
243
+ PresentationLogger.detail("Starting backtrace analysis...")
244
+ PresentationLogger.detail("First 5 backtrace lines:")
245
+ backtrace.first(5).each_with_index { |line, i| PresentationLogger.detail(" #{i}: #{line}") }
235
246
 
236
247
  # Use the exact working implementation from SelfRuby
237
248
  core_methods = %w[* + - / % ** == != < > <= >= <=> === =~ !~ & | ^ ~ << >> [] []= `]
238
249
  app_file_line = backtrace.find { |line| line.include?('/app/') }
239
250
  return [nil, nil] unless app_file_line
240
251
 
241
- puts "🔍 DEBUG: Found app file line: #{app_file_line}"
252
+ PresentationLogger.detail("Found app file line: #{app_file_line}")
242
253
 
243
254
  if app_file_line =~ /(.+):(\d+):in `(.+)'/
244
255
  file_path = $1
245
256
  method_name = $3
246
257
 
247
- puts "🔍 DEBUG: Extracted file_path=#{file_path}, method_name=#{method_name}"
258
+ PresentationLogger.detail("Extracted file_path=#{file_path}, method_name=#{method_name}")
248
259
 
249
260
  # If it's a core method, look deeper in the backtrace
250
261
  if core_methods.include?(method_name)
251
- puts "🔍 DEBUG: #{method_name} is a core method, looking deeper..."
262
+ PresentationLogger.detail("#{method_name} is a core method, looking deeper...")
252
263
  deeper_app_line = backtrace.find do |line|
253
264
  line.include?('/app/') &&
254
265
  line =~ /in `(.+)'/ &&
@@ -260,14 +271,14 @@ module CodeHealer
260
271
  end
261
272
 
262
273
  if deeper_app_line
263
- puts "🔍 DEBUG: Found deeper app line: #{deeper_app_line}"
274
+ PresentationLogger.detail("Found deeper app line: #{deeper_app_line}")
264
275
  if deeper_app_line =~ /(.+):(\d+):in `(.+)'/
265
276
  file_path = $1
266
277
  method_name = $3
267
- puts "🔍 DEBUG: Updated to file_path=#{file_path}, method_name=#{method_name}"
278
+ PresentationLogger.detail("Updated to file_path=#{file_path}, method_name=#{method_name}")
268
279
  end
269
280
  else
270
- puts "🔍 DEBUG: No deeper app line found"
281
+ PresentationLogger.detail("No deeper app line found")
271
282
  end
272
283
  end
273
284
 
@@ -279,7 +290,7 @@ module CodeHealer
279
290
  method_name.include?('reduce') ||
280
291
  method_name.include?('sum')
281
292
  )
282
- puts "🔍 DEBUG: #{method_name} is a block/iterator, looking for containing method..."
293
+ PresentationLogger.detail("#{method_name} is a block/iterator, looking for containing method...")
283
294
  # Look for the FIRST valid method in the backtrace, not just any method
284
295
  containing_line = backtrace.find do |line|
285
296
  line.include?('/app/') &&
@@ -294,14 +305,14 @@ module CodeHealer
294
305
  end
295
306
 
296
307
  if containing_line
297
- puts "🔍 DEBUG: Found containing line: #{containing_line}"
308
+ PresentationLogger.detail("Found containing line: #{containing_line}")
298
309
  if containing_line =~ /(.+):(\d+):in `(.+)'/
299
310
  file_path = $1
300
311
  method_name = $3
301
- puts "🔍 DEBUG: Updated to file_path=#{file_path}, method_name=#{method_name}"
312
+ PresentationLogger.detail("Updated to file_path=#{file_path}, method_name=#{method_name}")
302
313
  end
303
314
  else
304
- puts "🔍 DEBUG: No containing line found"
315
+ PresentationLogger.detail("No containing line found")
305
316
  end
306
317
  end
307
318
 
@@ -320,13 +331,13 @@ module CodeHealer
320
331
  end
321
332
  end
322
333
 
323
- puts "🔍 DEBUG: Final result - class_name=#{class_name}, method_name=#{method_name}"
324
- puts "🔍 Extracted: #{class_name}##{method_name} from #{file_path}"
334
+ PresentationLogger.detail("Final result - class_name=#{class_name}, method_name=#{method_name}")
335
+ PresentationLogger.detail("Extracted: #{class_name}##{method_name} from #{file_path}")
325
336
  return [class_name, method_name]
326
337
  end
327
338
  end
328
339
 
329
- puts "🔍 DEBUG: No valid method found in backtrace"
340
+ PresentationLogger.detail("No valid method found in backtrace")
330
341
  [nil, nil]
331
342
  end
332
343
 
@@ -62,43 +62,43 @@ module CodeHealer
62
62
  end
63
63
 
64
64
  def apply_fixes_in_workspace(workspace_path, fixes, class_name, method_name)
65
- puts "🔧 [WORKSPACE] Starting fix application..."
66
- puts "🔧 [WORKSPACE] Workspace: #{workspace_path}"
67
- puts "🔧 [WORKSPACE] Class: #{class_name}, Method: #{method_name}"
68
- puts "🔧 [WORKSPACE] Fixes to apply: #{fixes.inspect}"
65
+ PresentationLogger.detail("Starting fix application...")
66
+ PresentationLogger.detail("Workspace: #{workspace_path}")
67
+ PresentationLogger.detail("Class: #{class_name}, Method: #{method_name}")
68
+ PresentationLogger.detail("Fixes to apply: #{fixes.inspect}")
69
69
 
70
70
  begin
71
- puts "🔧 [WORKSPACE] Processing #{fixes.length} fixes..."
71
+ PresentationLogger.detail("Processing #{fixes.length} fixes...")
72
72
  # Apply each fix to the workspace
73
73
  fixes.each_with_index do |fix, index|
74
- puts "🔧 [WORKSPACE] Processing fix #{index + 1}: #{fix.inspect}"
74
+ PresentationLogger.detail("Processing fix #{index + 1}: #{fix.inspect}")
75
75
  file_path = File.join(workspace_path, fix[:file_path])
76
- puts "🔧 [WORKSPACE] Target file: #{file_path}"
77
- puts "🔧 [WORKSPACE] File exists: #{File.exist?(file_path)}"
76
+ PresentationLogger.detail("Target file: #{file_path}")
77
+ PresentationLogger.detail("File exists: #{File.exist?(file_path)}")
78
78
 
79
79
  next unless File.exist?(file_path)
80
80
 
81
- puts "🔧 [WORKSPACE] Creating backup..."
81
+ PresentationLogger.detail("Creating backup...")
82
82
  # Backup original file
83
83
  backup_file(file_path)
84
84
 
85
- puts "🔧 [WORKSPACE] Applying fix to file..."
85
+ PresentationLogger.detail("Applying fix to file...")
86
86
  # Apply the fix
87
87
  apply_fix_to_file(file_path, fix[:new_code], class_name, method_name)
88
88
  end
89
89
 
90
90
  # Show workspace Git status after applying fixes
91
91
  Dir.chdir(workspace_path) do
92
- puts "🔧 [WORKSPACE] Git status after fixes:"
92
+ PresentationLogger.detail("Git status after fixes:")
93
93
  system("git status --porcelain")
94
- puts "🔧 [WORKSPACE] Git diff after fixes:"
94
+ PresentationLogger.detail("Git diff after fixes:")
95
95
  system("git diff")
96
96
  end
97
97
 
98
- puts "Fixes applied successfully in workspace"
98
+ PresentationLogger.success("Fixes applied successfully in workspace")
99
99
  true
100
100
  rescue => e
101
- puts "Failed to apply fixes in workspace: #{e.message}"
101
+ PresentationLogger.error("Failed to apply fixes in workspace: #{e.message}")
102
102
  false
103
103
  end
104
104
  end
@@ -118,28 +118,25 @@ module CodeHealer
118
118
  return false
119
119
  end
120
120
 
121
- # Optionally skip heavy tests in demo mode
122
- unless CodeHealer::ConfigManager.demo_skip_tests?
123
- # Run tests if available
124
- if File.exist?('Gemfile')
125
- bundle_check = system("bundle check >/dev/null 2>&1")
126
- unless bundle_check
127
- PresentationLogger.error("Bundle check failed")
128
- return false
129
- end
130
-
131
- # Run tests if RSpec is available
132
- if File.exist?('spec') || File.exist?('test')
133
- test_result = system("bundle exec rspec --dry-run >/dev/null 2>&1") ||
134
- system("bundle exec rake test:prepare >/dev/null 2>&1")
135
- PresentationLogger.detail("Test preparation: #{test_result ? 'passed' : 'skipped'}")
136
- end
121
+ # Run tests if available
122
+ if File.exist?('Gemfile')
123
+ bundle_check = system("bundle check >/dev/null 2>&1")
124
+ unless bundle_check
125
+ PresentationLogger.error("Bundle check failed")
126
+ return false
127
+ end
128
+
129
+ # Run tests if RSpec is available
130
+ if File.exist?('spec') || File.exist?('test')
131
+ test_result = system("bundle exec rspec --dry-run >/dev/null 2>&1") ||
132
+ system("bundle exec rake test:prepare >/dev/null 2>&1")
133
+ PresentationLogger.detail("Test preparation: #{test_result ? 'passed' : 'skipped'}")
137
134
  end
138
135
  end
139
-
140
- PresentationLogger.success("Validation passed")
141
- true
142
136
  end
137
+
138
+ PresentationLogger.success("Validation passed")
139
+ true
143
140
  rescue => e
144
141
  PresentationLogger.error("Validation failed: #{e.message}")
145
142
  false
@@ -147,7 +144,7 @@ module CodeHealer
147
144
  end
148
145
 
149
146
  def validate_workspace_for_commit(workspace_path)
150
- puts "🔍 [WORKSPACE] Validating workspace for commit..."
147
+ PresentationLogger.detail("Validating workspace for commit...")
151
148
 
152
149
  Dir.chdir(workspace_path) do
153
150
  # Check for any temporary files that might have been added
@@ -156,30 +153,30 @@ module CodeHealer
156
153
 
157
154
  all_files = (staged_files + working_files).uniq.reject(&:empty?)
158
155
 
159
- puts "🔍 [WORKSPACE] Files to be committed: #{all_files.join(', ')}"
156
+ PresentationLogger.detail("Files to be committed: #{all_files.join(', ')}")
160
157
 
161
158
  # Check for any temporary files
162
159
  temp_files = all_files.select { |file| should_skip_file?(file) }
163
160
 
164
161
  if temp_files.any?
165
- puts "⚠️ [WORKSPACE] WARNING: Temporary files detected in commit:"
166
- temp_files.each { |file| puts " - #{file}" }
162
+ PresentationLogger.warn("Temporary files detected in commit:")
163
+ temp_files.each { |file| PresentationLogger.detail(" - #{file}") }
167
164
 
168
165
  # Remove them from staging
169
166
  temp_files.each do |file|
170
- puts "🗑️ [WORKSPACE] Removing temporary file from staging: #{file}"
167
+ PresentationLogger.detail("Removing temporary file from staging: #{file}")
171
168
  system("git reset HEAD '#{file}' 2>/dev/null || true")
172
169
  end
173
170
 
174
- puts "🔍 [WORKSPACE] Temporary files removed from staging"
171
+ PresentationLogger.detail("Temporary files removed from staging")
175
172
  return false
176
173
  end
177
174
 
178
- puts "✅ [WORKSPACE] Workspace validation passed - no temporary files detected"
175
+ PresentationLogger.detail("Workspace validation passed - no temporary files detected")
179
176
  return true
180
177
  end
181
178
  rescue => e
182
- puts "❌ [WORKSPACE] Workspace validation failed: #{e.message}"
179
+ PresentationLogger.error("Workspace validation failed: #{e.message}")
183
180
  return false
184
181
  end
185
182
 
@@ -333,7 +330,23 @@ module CodeHealer
333
330
 
334
331
  puts "🔗 Creating PR for repository: #{repo_name}"
335
332
 
336
- client = Octokit::Client.new(access_token: github_token)
333
+ # Configure Octokit with better error handling
334
+ client = Octokit::Client.new(
335
+ access_token: github_token,
336
+ api_endpoint: 'https://api.github.com',
337
+ web_endpoint: 'https://github.com',
338
+ auto_paginate: true,
339
+ per_page: 100
340
+ )
341
+
342
+ # Test the connection first
343
+ begin
344
+ user = client.user
345
+ puts "✅ GitHub authentication successful for user: #{user.login}"
346
+ rescue => auth_error
347
+ puts "❌ GitHub authentication failed: #{auth_error.message}"
348
+ return nil
349
+ end
337
350
 
338
351
  # Create pull request
339
352
  pr = client.create_pull_request(
@@ -348,8 +361,21 @@ module CodeHealer
348
361
 
349
362
  puts "✅ Pull request created successfully: #{pr.html_url}"
350
363
  pr.html_url
364
+ rescue Octokit::Unauthorized => e
365
+ puts "❌ GitHub authentication failed: #{e.message}"
366
+ puts "💡 Check your GitHub token permissions and validity"
367
+ nil
368
+ rescue Octokit::NotFound => e
369
+ puts "❌ Repository not found: #{e.message}"
370
+ puts "💡 Check repository name and access permissions"
371
+ nil
372
+ rescue Octokit::UnprocessableEntity => e
373
+ puts "❌ Invalid pull request data: #{e.message}"
374
+ puts "💡 Check branch names and repository state"
375
+ nil
351
376
  rescue => e
352
377
  puts "❌ Failed to create pull request: #{e.message}"
378
+ puts "💡 Error class: #{e.class}"
353
379
  puts "💡 Check your GitHub token and repository access"
354
380
  nil
355
381
  end
@@ -398,66 +424,66 @@ module CodeHealer
398
424
  end
399
425
 
400
426
  def create_persistent_workspace(repo_path, workspace_path, branch_name)
401
- puts "🏥 [WORKSPACE] Creating new persistent workspace..."
427
+ PresentationLogger.detail("Creating new persistent workspace...")
402
428
 
403
429
  # Get the GitHub remote URL
404
430
  Dir.chdir(repo_path) do
405
431
  remote_url = `git config --get remote.origin.url`.strip
406
432
  if remote_url.empty?
407
- puts "❌ [WORKSPACE] No remote origin found in #{repo_path}"
433
+ PresentationLogger.error("No remote origin found in #{repo_path}")
408
434
  return false
409
435
  end
410
436
 
411
- puts "🏥 [WORKSPACE] Cloning from: #{remote_url}"
412
- puts "🏥 [WORKSPACE] To workspace: #{workspace_path}"
437
+ PresentationLogger.detail("Cloning from: #{remote_url}")
438
+ PresentationLogger.detail("To workspace: #{workspace_path}")
413
439
 
414
440
  # Clone the full repository for persistent use
415
441
  result = system("git clone #{remote_url} #{workspace_path}")
416
442
  if result
417
- puts "🏥 [WORKSPACE] Repository cloned successfully"
443
+ PresentationLogger.detail("Repository cloned successfully")
418
444
  # Now checkout to the target branch
419
445
  checkout_to_branch(workspace_path, branch_name, repo_path)
420
446
  else
421
- puts "❌ [WORKSPACE] Failed to clone repository"
447
+ PresentationLogger.error("Failed to clone repository")
422
448
  return false
423
449
  end
424
450
  end
425
451
  end
426
452
 
427
453
  def checkout_to_branch(workspace_path, branch_name, repo_path)
428
- puts "🏥 [WORKSPACE] Checking out to target branch..."
454
+ PresentationLogger.detail("Checking out to target branch...")
429
455
 
430
456
  # Determine target branch
431
457
  target_branch = branch_name || CodeHealer::ConfigManager.pr_target_branch || get_default_branch(repo_path)
432
- puts "🏥 [WORKSPACE] Target branch: #{target_branch}"
458
+ PresentationLogger.detail("Target branch: #{target_branch}")
433
459
 
434
460
  Dir.chdir(workspace_path) do
435
461
  # Fetch latest changes
436
- puts "🏥 [WORKSPACE] Fetching latest changes..."
462
+ PresentationLogger.detail("Fetching latest changes...")
437
463
  system("git fetch origin")
438
464
 
439
465
  # Check if branch exists locally
440
466
  local_branch_exists = system("git show-ref --verify --quiet refs/heads/#{target_branch}")
441
467
 
442
468
  if local_branch_exists
443
- puts "🏥 [WORKSPACE] Checking out existing local branch: #{target_branch}"
469
+ PresentationLogger.detail("Checking out existing local branch: #{target_branch}")
444
470
  system("git checkout #{target_branch}")
445
471
  else
446
- puts "🏥 [WORKSPACE] Checking out remote branch: #{target_branch}"
472
+ PresentationLogger.detail("Checking out remote branch: #{target_branch}")
447
473
  system("git checkout -b #{target_branch} origin/#{target_branch}")
448
474
  end
449
475
 
450
476
  # Pull latest changes
451
- puts "🏥 [WORKSPACE] Pulling latest changes..."
477
+ PresentationLogger.detail("Pulling latest changes...")
452
478
  system("git pull origin #{target_branch}")
453
479
 
454
480
  # Ensure workspace is clean
455
- puts "🏥 [WORKSPACE] Ensuring workspace is clean..."
481
+ PresentationLogger.detail("Ensuring workspace is clean...")
456
482
  system("git reset --hard HEAD")
457
483
  system("git clean -fd")
458
484
 
459
485
  # Remove any tracked temporary files that shouldn't be committed - AGGRESSIVE cleanup
460
- puts "🏥 [WORKSPACE] Removing tracked temporary files..."
486
+ PresentationLogger.detail("Removing tracked temporary files...")
461
487
 
462
488
  # Remove root level temporary directories
463
489
  system("git rm -r --cached tmp/ 2>/dev/null || true")
@@ -477,7 +503,7 @@ module CodeHealer
477
503
  system("find . -name '*.log' -exec git rm --cached {} + 2>/dev/null || true")
478
504
  system("find . -name '*.cache' -exec git rm --cached {} + 2>/dev/null || true")
479
505
 
480
- puts "🏥 [WORKSPACE] Successfully checked out to: #{target_branch}"
506
+ PresentationLogger.detail("Successfully checked out to: #{target_branch}")
481
507
  end
482
508
  end
483
509
 
@@ -503,25 +529,26 @@ module CodeHealer
503
529
  end
504
530
 
505
531
  def add_only_relevant_files(workspace_path)
506
- puts "📁 [WORKSPACE] Adding only relevant files, respecting .gitignore..."
532
+ PresentationLogger.detail("Adding only relevant files, respecting .gitignore...")
507
533
 
508
534
  Dir.chdir(workspace_path) do
509
535
  # First, ensure .gitignore is respected
510
536
  if File.exist?('.gitignore')
511
- puts "📁 [WORKSPACE] Using repository's .gitignore file"
537
+ PresentationLogger.detail("Using repository's .gitignore file")
512
538
  else
513
- puts "📁 [WORKSPACE] No .gitignore found, using default patterns"
539
+ PresentationLogger.detail("No .gitignore found, using default patterns")
514
540
  end
515
541
 
516
542
  # Get list of modified files
517
543
  modified_files = `git status --porcelain | grep '^ M\\|^M \\|^A ' | awk '{print $2}'`.strip.split("\n")
544
+
518
545
 
519
546
  if modified_files.empty?
520
- puts "📁 [WORKSPACE] No modified files to add"
547
+ PresentationLogger.detail("No modified files to add")
521
548
  return
522
549
  end
523
550
 
524
- puts "📁 [WORKSPACE] Modified files: #{modified_files.join(', ')}"
551
+ PresentationLogger.detail("Modified files: #{modified_files.join(', ')}")
525
552
 
526
553
  # Add each modified file individually
527
554
  modified_files.each do |file|
@@ -529,15 +556,15 @@ module CodeHealer
529
556
 
530
557
  # Skip temporary and generated files
531
558
  if should_skip_file?(file)
532
- puts "📁 [WORKSPACE] Skipping temporary file: #{file}"
559
+ PresentationLogger.detail("Skipping temporary file: #{file}")
533
560
  next
534
561
  end
535
562
 
536
- puts "📁 [WORKSPACE] Adding file: #{file}"
563
+ PresentationLogger.detail("Adding file: #{file}")
537
564
  system("git add '#{file}'")
538
565
  end
539
566
 
540
- puts "📁 [WORKSPACE] File addition completed"
567
+ PresentationLogger.detail("File addition completed")
541
568
  end
542
569
  end
543
570
 
@@ -594,7 +621,7 @@ module CodeHealer
594
621
 
595
622
  # Additional check: if path contains 'tmp' or 'log' anywhere, skip it
596
623
  if file_path.include?('tmp') || file_path.include?('log')
597
- puts "📁 [WORKSPACE] Skipping file containing 'tmp' or 'log': #{file_path}"
624
+ PresentationLogger.detail("Skipping file containing 'tmp' or 'log': #{file_path}")
598
625
  return true
599
626
  end
600
627
 
@@ -1,5 +1,6 @@
1
1
  require_relative 'mcp_tools'
2
2
  require_relative 'mcp_prompts'
3
+ require_relative 'presentation_logger'
3
4
 
4
5
  module CodeHealer
5
6
  class McpServer
@@ -45,7 +46,7 @@ module CodeHealer
45
46
  end
46
47
 
47
48
  def analyze_error(error, context)
48
- puts "🧠 MCP analyzing error: #{error.class} - #{error.message}"
49
+ PresentationLogger.detail("MCP analyzing error: #{error.class} - #{error.message}")
49
50
 
50
51
  # Extract class and method names from context
51
52
  class_name = context[:class_name] || 'UnknownClass'
@@ -61,11 +62,11 @@ module CodeHealer
61
62
  server_context: { codebase_context: context }
62
63
  )
63
64
 
64
- puts "MCP analysis complete"
65
+ PresentationLogger.detail("MCP analysis complete")
65
66
  # Parse the JSON response from MCP tool
66
67
  JSON.parse(result.content.first[:text])
67
68
  else
68
- puts "⚠️ ErrorAnalysisTool not available, using fallback analysis"
69
+ PresentationLogger.detail("ErrorAnalysisTool not available, using fallback analysis")
69
70
  # Fallback analysis
70
71
  {
71
72
  severity: 'medium',
@@ -78,13 +79,13 @@ module CodeHealer
78
79
  end
79
80
 
80
81
  def generate_contextual_fix(error, analysis, context)
81
- puts "🧠 MCP generating contextual fix..."
82
+ PresentationLogger.detail("MCP generating contextual fix...")
82
83
 
83
84
  # Extract class and method names from context
84
85
  class_name = context[:class_name] || 'UnknownClass'
85
86
  method_name = context[:method_name] || 'unknown_method'
86
87
 
87
- puts "🔍 Debug: class_name = #{class_name}, method_name = #{method_name}"
88
+ PresentationLogger.detail("Debug: class_name = #{class_name}, method_name = #{method_name}")
88
89
 
89
90
  # Use MCP tool to generate fix
90
91
  if defined?(CodeFixTool)
@@ -101,11 +102,11 @@ module CodeHealer
101
102
  }
102
103
  )
103
104
 
104
- puts "MCP generated intelligent fix"
105
+ PresentationLogger.detail("MCP generated intelligent fix")
105
106
  # Parse the JSON response from MCP tool
106
107
  JSON.parse(result.content.first[:text])
107
108
  else
108
- puts "⚠️ CodeFixTool not available, using fallback fix generation"
109
+ PresentationLogger.detail("CodeFixTool not available, using fallback fix generation")
109
110
  # Fallback fix generation
110
111
  generate_fallback_fix(error, class_name, method_name)
111
112
  end
@@ -1,5 +1,5 @@
1
1
  module CodeHealer
2
- # Presentation-focused logger for conference demos and clean operator output
2
+ # Presentation-focused logger for clean operator output
3
3
  class PresentationLogger
4
4
  class << self
5
5
  def verbose?
@@ -60,9 +60,9 @@ module CodeHealer
60
60
  # Show only the first 3 relevant lines for presentation
61
61
  relevant_lines = backtrace_array.first(3).map do |line|
62
62
  # Extract just the file and line number for cleaner display
63
- if line.match?(/^(.+\.rb):(\d+):in/)
64
- file = File.basename($1)
65
- line_num = $2
63
+ if (m = line.match(/^(.+\.rb):(\d+):in/))
64
+ file = File.basename(m[1])
65
+ line_num = m[2]
66
66
  method = line.match(/in `(.+)'/)&.[](1) || 'unknown'
67
67
  "#{file}:#{line_num} in #{method}"
68
68
  else
@@ -414,34 +414,7 @@ evolution_method = case evolution_method.downcase
414
414
 
415
415
  fallback_to_api = ask_for_yes_no("Fallback to API if Claude Code fails?", default: true)
416
416
 
417
- # Demo Mode Configuration
418
- puts
419
- puts "🎭 Demo Mode Configuration:"
420
- puts "Demo mode optimizes CodeHealer for fast demonstrations and presentations:"
421
- puts "- Skips test generation for faster response times"
422
- puts "- Skips pull request creation for immediate results"
423
- puts "- Uses optimized Claude prompts for quick fixes"
424
- puts
425
-
426
- enable_demo_mode = ask_for_yes_no("Enable demo mode for fast demonstrations?", default: false)
427
-
428
- demo_config = {}
429
- if enable_demo_mode
430
- demo_config[:skip_tests] = ask_for_yes_no("Skip test generation in demo mode?", default: true)
431
-
432
-
433
- puts
434
- puts "🚀 Demo mode will significantly speed up healing operations!"
435
- puts " Perfect for conference talks and live demonstrations."
436
-
437
- # Add demo-specific instructions
438
- puts
439
- puts "📋 Demo Mode Features:"
440
- puts " - Timeout reduced to 60 seconds for quick responses"
441
- puts " - Sticky workspace enabled for faster context loading"
442
- puts " - Claude session persistence for better performance"
443
- puts " - Tests skipped for immediate results (PRs still created)"
444
- end
417
+
445
418
 
446
419
  # Create configuration files
447
420
  puts
@@ -481,13 +454,6 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
481
454
  # CodeHealer Configuration
482
455
  enabled: true
483
456
 
484
- # Allowed classes for healing (customize as needed)
485
- allowed_classes:
486
- - User
487
- - Order
488
- - PaymentProcessor
489
- - OrderProcessor
490
-
491
457
  # Excluded classes (never touch these)
492
458
  excluded_classes:
493
459
  - ApplicationController
@@ -513,9 +479,9 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
513
479
  # Claude Code Terminal Configuration
514
480
  claude_code:
515
481
  enabled: #{evolution_method == 'claude_code_terminal' || evolution_method == 'hybrid'}
516
- timeout: #{enable_demo_mode ? 60 : 300} # Shorter timeout for demo mode
482
+ timeout: 300
517
483
  max_file_changes: 10
518
- include_tests: #{!enable_demo_mode || !demo_config[:skip_tests]}
484
+ include_tests: true
519
485
  persist_session: true # Keep Claude session alive for faster responses
520
486
  ignore:
521
487
  - "tmp/"
@@ -608,15 +574,15 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
608
574
  max_evolutions_per_day: 10
609
575
 
610
576
  # Notification Configuration (optional)
577
+ # Test-Fix Iteration Configuration
578
+ test_fix:
579
+ max_iterations: 2
611
580
  notifications:
612
581
  enabled: false
613
582
  slack_webhook: ""
614
583
  email_notifications: false
615
584
 
616
- # Demo Mode Configuration
617
- demo:
618
- enabled: #{enable_demo_mode}
619
- skip_tests: #{demo_config[:skip_tests] || false}
585
+
620
586
 
621
587
  # Performance Configuration
622
588
  performance:
@@ -631,7 +597,7 @@ create_file_with_content('.env', env_content, dry_run: options[:dry_run])
631
597
  cleanup_after_hours: #{cleanup_after_hours}
632
598
  max_workspaces: 10
633
599
  clone_strategy: "branch" # Options: branch, full_repo
634
- sticky_workspace: #{enable_demo_mode} # Reuse workspace for faster demo responses
600
+ sticky_workspace: false
635
601
  YAML
636
602
 
637
603
  create_file_with_content('config/code_healer.yml', config_content, dry_run: options[:dry_run])
@@ -711,9 +677,7 @@ puts
711
677
  puts " - Your code will be cloned to: #{code_heal_directory}"
712
678
  puts " - This ensures safe, isolated healing without affecting your running server"
713
679
  puts " - Workspaces are automatically cleaned up after #{cleanup_after_hours} hours"
714
- if enable_demo_mode
715
- puts " - Demo mode: Sticky workspace enabled for faster context loading"
716
- end
680
+
717
681
  puts
718
682
  puts "⚙️ Configuration:"
719
683
  puts " - code_healer.yml contains comprehensive settings with sensible defaults"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CodeHealer
4
- VERSION = "0.1.26"
4
+ VERSION = "0.1.33"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code_healer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.26
4
+ version: 0.1.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deepan Kumar
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-03 00:00:00.000000000 Z
11
+ date: 2025-09-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails