code_healer 0.1.26 → 0.1.32
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 +16 -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 +78 -49
- 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: 2a36985eccac2d4e1f55a411b9872478f6ec9e2f5285ebe969f6a839e600e37a
|
4
|
+
data.tar.gz: 7f1c43c592b9b6f6ed953ac70cc13855048d7f62222b65ddbf56b047a711ece5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 790508da0467e26544b1d89bab7caece0bc0afd0e8bc10cf1aee0166704d75a5ef717cab75049b32b6c51e296339df1e8a036194684b100c7ca7e840de5a8230
|
7
|
+
data.tar.gz: a34f3ec6ba73155e4adba2ed08ed30a3581dc587ec1d91df2386a69653ec46a83351d2f03bfae02e525e432c64410ea119e9293c44493b08210e7e4a4ba824bd
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## [0.1.32] - 2025-09-15
|
2
|
+
|
3
|
+
### Added
|
4
|
+
- Targeted RSpec test-fix loop after code changes in `ClaudeCodeEvolutionHandler`:
|
5
|
+
- Runs specs for files related to recent modifications
|
6
|
+
- Parses failures and re-invokes Claude with failure summary
|
7
|
+
- Iterates up to `test_fix.max_iterations` (default 2)
|
8
|
+
- Configuration: `test_fix.max_iterations` with sensible default in `ConfigManager` and `setup.rb`.
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
- Removed demo mode code and comments across gem: prompts, setup script, logger phrasing, and workspace manager.
|
12
|
+
- Switched class targeting to excluded-classes-only model; removed `allowed_classes` usage in config managers and setup generation.
|
13
|
+
|
14
|
+
### Notes
|
15
|
+
- Backwards compatible; no breaking API changes. Configure `test_fix.max_iterations` in `config/code_healer.yml` to tune retries.
|
16
|
+
|
1
17
|
# Changelog
|
2
18
|
|
3
19
|
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,8 +118,7 @@ module CodeHealer
|
|
118
118
|
return false
|
119
119
|
end
|
120
120
|
|
121
|
-
#
|
122
|
-
unless CodeHealer::ConfigManager.demo_skip_tests?
|
121
|
+
# Run tests if available
|
123
122
|
# Run tests if available
|
124
123
|
if File.exist?('Gemfile')
|
125
124
|
bundle_check = system("bundle check >/dev/null 2>&1")
|
@@ -147,7 +146,7 @@ module CodeHealer
|
|
147
146
|
end
|
148
147
|
|
149
148
|
def validate_workspace_for_commit(workspace_path)
|
150
|
-
|
149
|
+
PresentationLogger.detail("Validating workspace for commit...")
|
151
150
|
|
152
151
|
Dir.chdir(workspace_path) do
|
153
152
|
# Check for any temporary files that might have been added
|
@@ -156,30 +155,30 @@ module CodeHealer
|
|
156
155
|
|
157
156
|
all_files = (staged_files + working_files).uniq.reject(&:empty?)
|
158
157
|
|
159
|
-
|
158
|
+
PresentationLogger.detail("Files to be committed: #{all_files.join(', ')}")
|
160
159
|
|
161
160
|
# Check for any temporary files
|
162
161
|
temp_files = all_files.select { |file| should_skip_file?(file) }
|
163
162
|
|
164
163
|
if temp_files.any?
|
165
|
-
|
166
|
-
temp_files.each { |file|
|
164
|
+
PresentationLogger.warn("Temporary files detected in commit:")
|
165
|
+
temp_files.each { |file| PresentationLogger.detail(" - #{file}") }
|
167
166
|
|
168
167
|
# Remove them from staging
|
169
168
|
temp_files.each do |file|
|
170
|
-
|
169
|
+
PresentationLogger.detail("Removing temporary file from staging: #{file}")
|
171
170
|
system("git reset HEAD '#{file}' 2>/dev/null || true")
|
172
171
|
end
|
173
172
|
|
174
|
-
|
173
|
+
PresentationLogger.detail("Temporary files removed from staging")
|
175
174
|
return false
|
176
175
|
end
|
177
176
|
|
178
|
-
|
177
|
+
PresentationLogger.detail("Workspace validation passed - no temporary files detected")
|
179
178
|
return true
|
180
179
|
end
|
181
180
|
rescue => e
|
182
|
-
|
181
|
+
PresentationLogger.error("Workspace validation failed: #{e.message}")
|
183
182
|
return false
|
184
183
|
end
|
185
184
|
|
@@ -333,7 +332,23 @@ module CodeHealer
|
|
333
332
|
|
334
333
|
puts "🔗 Creating PR for repository: #{repo_name}"
|
335
334
|
|
336
|
-
|
335
|
+
# Configure Octokit with better error handling
|
336
|
+
client = Octokit::Client.new(
|
337
|
+
access_token: github_token,
|
338
|
+
api_endpoint: 'https://api.github.com',
|
339
|
+
web_endpoint: 'https://github.com',
|
340
|
+
auto_paginate: true,
|
341
|
+
per_page: 100
|
342
|
+
)
|
343
|
+
|
344
|
+
# Test the connection first
|
345
|
+
begin
|
346
|
+
user = client.user
|
347
|
+
puts "✅ GitHub authentication successful for user: #{user.login}"
|
348
|
+
rescue => auth_error
|
349
|
+
puts "❌ GitHub authentication failed: #{auth_error.message}"
|
350
|
+
return nil
|
351
|
+
end
|
337
352
|
|
338
353
|
# Create pull request
|
339
354
|
pr = client.create_pull_request(
|
@@ -348,8 +363,21 @@ module CodeHealer
|
|
348
363
|
|
349
364
|
puts "✅ Pull request created successfully: #{pr.html_url}"
|
350
365
|
pr.html_url
|
366
|
+
rescue Octokit::Unauthorized => e
|
367
|
+
puts "❌ GitHub authentication failed: #{e.message}"
|
368
|
+
puts "💡 Check your GitHub token permissions and validity"
|
369
|
+
nil
|
370
|
+
rescue Octokit::NotFound => e
|
371
|
+
puts "❌ Repository not found: #{e.message}"
|
372
|
+
puts "💡 Check repository name and access permissions"
|
373
|
+
nil
|
374
|
+
rescue Octokit::UnprocessableEntity => e
|
375
|
+
puts "❌ Invalid pull request data: #{e.message}"
|
376
|
+
puts "💡 Check branch names and repository state"
|
377
|
+
nil
|
351
378
|
rescue => e
|
352
379
|
puts "❌ Failed to create pull request: #{e.message}"
|
380
|
+
puts "💡 Error class: #{e.class}"
|
353
381
|
puts "💡 Check your GitHub token and repository access"
|
354
382
|
nil
|
355
383
|
end
|
@@ -398,66 +426,66 @@ module CodeHealer
|
|
398
426
|
end
|
399
427
|
|
400
428
|
def create_persistent_workspace(repo_path, workspace_path, branch_name)
|
401
|
-
|
429
|
+
PresentationLogger.detail("Creating new persistent workspace...")
|
402
430
|
|
403
431
|
# Get the GitHub remote URL
|
404
432
|
Dir.chdir(repo_path) do
|
405
433
|
remote_url = `git config --get remote.origin.url`.strip
|
406
434
|
if remote_url.empty?
|
407
|
-
|
435
|
+
PresentationLogger.error("No remote origin found in #{repo_path}")
|
408
436
|
return false
|
409
437
|
end
|
410
438
|
|
411
|
-
|
412
|
-
|
439
|
+
PresentationLogger.detail("Cloning from: #{remote_url}")
|
440
|
+
PresentationLogger.detail("To workspace: #{workspace_path}")
|
413
441
|
|
414
442
|
# Clone the full repository for persistent use
|
415
443
|
result = system("git clone #{remote_url} #{workspace_path}")
|
416
444
|
if result
|
417
|
-
|
445
|
+
PresentationLogger.detail("Repository cloned successfully")
|
418
446
|
# Now checkout to the target branch
|
419
447
|
checkout_to_branch(workspace_path, branch_name, repo_path)
|
420
448
|
else
|
421
|
-
|
449
|
+
PresentationLogger.error("Failed to clone repository")
|
422
450
|
return false
|
423
451
|
end
|
424
452
|
end
|
425
453
|
end
|
426
454
|
|
427
455
|
def checkout_to_branch(workspace_path, branch_name, repo_path)
|
428
|
-
|
456
|
+
PresentationLogger.detail("Checking out to target branch...")
|
429
457
|
|
430
458
|
# Determine target branch
|
431
459
|
target_branch = branch_name || CodeHealer::ConfigManager.pr_target_branch || get_default_branch(repo_path)
|
432
|
-
|
460
|
+
PresentationLogger.detail("Target branch: #{target_branch}")
|
433
461
|
|
434
462
|
Dir.chdir(workspace_path) do
|
435
463
|
# Fetch latest changes
|
436
|
-
|
464
|
+
PresentationLogger.detail("Fetching latest changes...")
|
437
465
|
system("git fetch origin")
|
438
466
|
|
439
467
|
# Check if branch exists locally
|
440
468
|
local_branch_exists = system("git show-ref --verify --quiet refs/heads/#{target_branch}")
|
441
469
|
|
442
470
|
if local_branch_exists
|
443
|
-
|
471
|
+
PresentationLogger.detail("Checking out existing local branch: #{target_branch}")
|
444
472
|
system("git checkout #{target_branch}")
|
445
473
|
else
|
446
|
-
|
474
|
+
PresentationLogger.detail("Checking out remote branch: #{target_branch}")
|
447
475
|
system("git checkout -b #{target_branch} origin/#{target_branch}")
|
448
476
|
end
|
449
477
|
|
450
478
|
# Pull latest changes
|
451
|
-
|
479
|
+
PresentationLogger.detail("Pulling latest changes...")
|
452
480
|
system("git pull origin #{target_branch}")
|
453
481
|
|
454
482
|
# Ensure workspace is clean
|
455
|
-
|
483
|
+
PresentationLogger.detail("Ensuring workspace is clean...")
|
456
484
|
system("git reset --hard HEAD")
|
457
485
|
system("git clean -fd")
|
458
486
|
|
459
487
|
# Remove any tracked temporary files that shouldn't be committed - AGGRESSIVE cleanup
|
460
|
-
|
488
|
+
PresentationLogger.detail("Removing tracked temporary files...")
|
461
489
|
|
462
490
|
# Remove root level temporary directories
|
463
491
|
system("git rm -r --cached tmp/ 2>/dev/null || true")
|
@@ -477,7 +505,7 @@ module CodeHealer
|
|
477
505
|
system("find . -name '*.log' -exec git rm --cached {} + 2>/dev/null || true")
|
478
506
|
system("find . -name '*.cache' -exec git rm --cached {} + 2>/dev/null || true")
|
479
507
|
|
480
|
-
|
508
|
+
PresentationLogger.detail("Successfully checked out to: #{target_branch}")
|
481
509
|
end
|
482
510
|
end
|
483
511
|
|
@@ -503,25 +531,26 @@ module CodeHealer
|
|
503
531
|
end
|
504
532
|
|
505
533
|
def add_only_relevant_files(workspace_path)
|
506
|
-
|
534
|
+
PresentationLogger.detail("Adding only relevant files, respecting .gitignore...")
|
507
535
|
|
508
536
|
Dir.chdir(workspace_path) do
|
509
537
|
# First, ensure .gitignore is respected
|
510
538
|
if File.exist?('.gitignore')
|
511
|
-
|
539
|
+
PresentationLogger.detail("Using repository's .gitignore file")
|
512
540
|
else
|
513
|
-
|
541
|
+
PresentationLogger.detail("No .gitignore found, using default patterns")
|
514
542
|
end
|
515
543
|
|
516
544
|
# Get list of modified files
|
517
545
|
modified_files = `git status --porcelain | grep '^ M\\|^M \\|^A ' | awk '{print $2}'`.strip.split("\n")
|
546
|
+
|
518
547
|
|
519
548
|
if modified_files.empty?
|
520
|
-
|
549
|
+
PresentationLogger.detail("No modified files to add")
|
521
550
|
return
|
522
551
|
end
|
523
552
|
|
524
|
-
|
553
|
+
PresentationLogger.detail("Modified files: #{modified_files.join(', ')}")
|
525
554
|
|
526
555
|
# Add each modified file individually
|
527
556
|
modified_files.each do |file|
|
@@ -529,15 +558,15 @@ module CodeHealer
|
|
529
558
|
|
530
559
|
# Skip temporary and generated files
|
531
560
|
if should_skip_file?(file)
|
532
|
-
|
561
|
+
PresentationLogger.detail("Skipping temporary file: #{file}")
|
533
562
|
next
|
534
563
|
end
|
535
564
|
|
536
|
-
|
565
|
+
PresentationLogger.detail("Adding file: #{file}")
|
537
566
|
system("git add '#{file}'")
|
538
567
|
end
|
539
568
|
|
540
|
-
|
569
|
+
PresentationLogger.detail("File addition completed")
|
541
570
|
end
|
542
571
|
end
|
543
572
|
|
@@ -594,7 +623,7 @@ module CodeHealer
|
|
594
623
|
|
595
624
|
# Additional check: if path contains 'tmp' or 'log' anywhere, skip it
|
596
625
|
if file_path.include?('tmp') || file_path.include?('log')
|
597
|
-
|
626
|
+
PresentationLogger.detail("Skipping file containing 'tmp' or 'log': #{file_path}")
|
598
627
|
return true
|
599
628
|
end
|
600
629
|
|
@@ -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.32
|
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
|