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 +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/code_healer/claude_code_evolution_handler.rb +116 -25
- data/lib/code_healer/config_manager.rb +16 -23
- data/lib/code_healer/core.rb +36 -25
- data/lib/code_healer/healing_workspace_manager.rb +93 -66
- data/lib/code_healer/mcp_server.rb +8 -7
- data/lib/code_healer/presentation_logger.rb +4 -4
- data/lib/code_healer/setup.rb +9 -45
- data/lib/code_healer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c3a36c401bef8aaddaf43a1b582bd0d398dc3f6d02e1618bc913327110eff02
|
4
|
+
data.tar.gz: 55375ba10a91f2c3ec1bea92c77034d1d5e7044c0a9bb6ca1f0a260531d03732
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
118
|
-
if
|
119
|
-
command += " --append-system-prompt '
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
369
|
-
'enabled' => false,
|
370
|
-
'skip_tests' => true
|
371
|
-
},
|
364
|
+
|
372
365
|
'git' => {
|
373
366
|
'auto_commit' => true,
|
374
367
|
'auto_push' => true,
|
data/lib/code_healer/core.rb
CHANGED
@@ -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
|
-
#
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
233
|
-
|
234
|
-
backtrace.first(5).each_with_index { |line, i|
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
278
|
+
PresentationLogger.detail("Updated to file_path=#{file_path}, method_name=#{method_name}")
|
268
279
|
end
|
269
280
|
else
|
270
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
312
|
+
PresentationLogger.detail("Updated to file_path=#{file_path}, method_name=#{method_name}")
|
302
313
|
end
|
303
314
|
else
|
304
|
-
|
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
|
-
|
324
|
-
|
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
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
74
|
+
PresentationLogger.detail("Processing fix #{index + 1}: #{fix.inspect}")
|
75
75
|
file_path = File.join(workspace_path, fix[:file_path])
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
+
PresentationLogger.detail("Creating backup...")
|
82
82
|
# Backup original file
|
83
83
|
backup_file(file_path)
|
84
84
|
|
85
|
-
|
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
|
-
|
92
|
+
PresentationLogger.detail("Git status after fixes:")
|
93
93
|
system("git status --porcelain")
|
94
|
-
|
94
|
+
PresentationLogger.detail("Git diff after fixes:")
|
95
95
|
system("git diff")
|
96
96
|
end
|
97
97
|
|
98
|
-
|
98
|
+
PresentationLogger.success("Fixes applied successfully in workspace")
|
99
99
|
true
|
100
100
|
rescue => e
|
101
|
-
|
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
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
166
|
-
temp_files.each { |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
|
-
|
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
|
-
|
171
|
+
PresentationLogger.detail("Temporary files removed from staging")
|
175
172
|
return false
|
176
173
|
end
|
177
174
|
|
178
|
-
|
175
|
+
PresentationLogger.detail("Workspace validation passed - no temporary files detected")
|
179
176
|
return true
|
180
177
|
end
|
181
178
|
rescue => e
|
182
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
433
|
+
PresentationLogger.error("No remote origin found in #{repo_path}")
|
408
434
|
return false
|
409
435
|
end
|
410
436
|
|
411
|
-
|
412
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
458
|
+
PresentationLogger.detail("Target branch: #{target_branch}")
|
433
459
|
|
434
460
|
Dir.chdir(workspace_path) do
|
435
461
|
# Fetch latest changes
|
436
|
-
|
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
|
-
|
469
|
+
PresentationLogger.detail("Checking out existing local branch: #{target_branch}")
|
444
470
|
system("git checkout #{target_branch}")
|
445
471
|
else
|
446
|
-
|
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
|
-
|
477
|
+
PresentationLogger.detail("Pulling latest changes...")
|
452
478
|
system("git pull origin #{target_branch}")
|
453
479
|
|
454
480
|
# Ensure workspace is clean
|
455
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
537
|
+
PresentationLogger.detail("Using repository's .gitignore file")
|
512
538
|
else
|
513
|
-
|
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
|
-
|
547
|
+
PresentationLogger.detail("No modified files to add")
|
521
548
|
return
|
522
549
|
end
|
523
550
|
|
524
|
-
|
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
|
-
|
559
|
+
PresentationLogger.detail("Skipping temporary file: #{file}")
|
533
560
|
next
|
534
561
|
end
|
535
562
|
|
536
|
-
|
563
|
+
PresentationLogger.detail("Adding file: #{file}")
|
537
564
|
system("git add '#{file}'")
|
538
565
|
end
|
539
566
|
|
540
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
64
|
-
file = File.basename(
|
65
|
-
line_num =
|
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
|
data/lib/code_healer/setup.rb
CHANGED
@@ -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
|
-
|
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:
|
482
|
+
timeout: 300
|
517
483
|
max_file_changes: 10
|
518
|
-
include_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
|
-
|
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:
|
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
|
-
|
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"
|
data/lib/code_healer/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|