code_healer 0.1.3 → 0.1.5
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 +20 -0
- data/lib/code_healer/config_manager.rb +28 -0
- data/lib/code_healer/evolution_job.rb +118 -48
- data/lib/code_healer/healing_job.rb +160 -140
- data/lib/code_healer/healing_workspace_manager.rb +296 -0
- data/lib/code_healer/setup.rb +32 -8
- data/lib/code_healer/version.rb +1 -1
- data/lib/code_healer.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 053ae42e7bf5fe56e20fefb00f6a5474bf2e4e74622738f81b45073bfe735da3
|
4
|
+
data.tar.gz: e3349ed04f2cb27fec077f0003f5bde4b63b9a9a220158bbfb32b864ea1af75b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2bac061a4947215ba717645dccf034ebf1e130eb98f58861d4f88ad8249d17cd83380853b965a1b7f539a5c6af9bbeefd795eb45b521023f447ac99cc222f8e1
|
7
|
+
data.tar.gz: da855be2022f1f6e1cd67dd48ea8a42985444b20895560fd34fe506734d2837fde99a374411c07f10622657b2acd5fc4ed329a5f04990aea6b30575ef0f3b5c2
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [0.1.5] - 2025-01-14
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
- **Duplicate HealingJob class definition** that was preventing isolated healing workspace system from working
|
12
|
+
- **Class loading conflict** between old and new healing logic
|
13
|
+
- **Isolated healing workspace system** now properly activated
|
14
|
+
|
15
|
+
## [0.1.4] - 2025-01-14
|
16
|
+
|
17
|
+
### Added
|
18
|
+
- **Comprehensive logging** for isolated healing workspace system
|
19
|
+
- **Detailed workspace creation logs** showing each step of the process
|
20
|
+
- **Clone operation logging** with success/failure status
|
21
|
+
- **Fix application logging** in isolated environment
|
22
|
+
- **Workspace cleanup logging** for debugging
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
- **Workspace configuration reading** to handle both string and symbol keys
|
26
|
+
- **Branch name sanitization** to prevent invalid Git branch names
|
27
|
+
|
8
28
|
## [0.1.3] - 2025-01-14
|
9
29
|
|
10
30
|
### Added
|
@@ -99,6 +99,27 @@ module CodeHealer
|
|
99
99
|
def git_settings
|
100
100
|
config['git'] || {}
|
101
101
|
end
|
102
|
+
|
103
|
+
# Code Heal Directory Configuration
|
104
|
+
def code_heal_directory_config
|
105
|
+
config['code_heal_directory'] || {}
|
106
|
+
end
|
107
|
+
|
108
|
+
def code_heal_directory_path
|
109
|
+
code_heal_directory_config['path'] || '/tmp/code_healer_workspaces'
|
110
|
+
end
|
111
|
+
|
112
|
+
def auto_cleanup_workspaces?
|
113
|
+
code_heal_directory_config['auto_cleanup'] != false
|
114
|
+
end
|
115
|
+
|
116
|
+
def workspace_cleanup_after_hours
|
117
|
+
code_heal_directory_config['cleanup_after_hours'] || 24
|
118
|
+
end
|
119
|
+
|
120
|
+
def max_workspaces
|
121
|
+
code_heal_directory_config['max_workspaces'] || 10
|
122
|
+
end
|
102
123
|
|
103
124
|
def pull_request_settings
|
104
125
|
config['pull_request'] || {}
|
@@ -267,6 +288,13 @@ module CodeHealer
|
|
267
288
|
'safety' => {
|
268
289
|
'backup_before_evolution' => true,
|
269
290
|
'rollback_on_syntax_error' => true
|
291
|
+
},
|
292
|
+
'code_heal_directory' => {
|
293
|
+
'path' => '/tmp/code_healer_workspaces',
|
294
|
+
'auto_cleanup' => true,
|
295
|
+
'cleanup_after_hours' => 24,
|
296
|
+
'max_workspaces' => 10,
|
297
|
+
'clone_strategy' => 'branch'
|
270
298
|
}
|
271
299
|
}
|
272
300
|
end
|
@@ -9,18 +9,39 @@ class EvolutionJob
|
|
9
9
|
# Reconstruct the error object
|
10
10
|
error = reconstruct_error(error_data)
|
11
11
|
|
12
|
-
#
|
13
|
-
|
12
|
+
# Create isolated healing workspace
|
13
|
+
workspace_path = create_healing_workspace(class_name, method_name)
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
15
|
+
begin
|
16
|
+
# Apply fixes in isolated environment
|
17
|
+
success = apply_fixes_in_workspace(workspace_path, error, class_name, method_name)
|
18
|
+
|
19
|
+
if success
|
20
|
+
# Test fixes in isolated environment
|
21
|
+
test_success = CodeHealer::HealingWorkspaceManager.test_fixes_in_workspace(workspace_path)
|
22
|
+
|
23
|
+
if test_success
|
24
|
+
# Merge back to main repo
|
25
|
+
healing_branch = CodeHealer::HealingWorkspaceManager.merge_fixes_back(
|
26
|
+
Rails.root.to_s,
|
27
|
+
workspace_path,
|
28
|
+
CodeHealer::ConfigManager.git_settings['pr_target_branch'] || 'main'
|
29
|
+
)
|
30
|
+
|
31
|
+
if healing_branch
|
32
|
+
puts "✅ Fixes applied, tested, and merged successfully! Branch: #{healing_branch}"
|
33
|
+
else
|
34
|
+
puts "⚠️ Fixes applied and tested, but merge failed"
|
35
|
+
end
|
36
|
+
else
|
37
|
+
puts "⚠️ Fixes applied but failed tests, not merging back"
|
38
|
+
end
|
39
|
+
else
|
40
|
+
puts "❌ Failed to apply fixes in workspace"
|
41
|
+
end
|
42
|
+
ensure
|
43
|
+
# Clean up workspace
|
44
|
+
cleanup_workspace(workspace_path)
|
24
45
|
end
|
25
46
|
|
26
47
|
puts "✅ Evolution Job Completed: #{class_name}##{method_name}"
|
@@ -32,35 +53,59 @@ class EvolutionJob
|
|
32
53
|
|
33
54
|
private
|
34
55
|
|
35
|
-
def
|
36
|
-
|
37
|
-
error_class = Object.const_get(error_data['class'])
|
38
|
-
error = error_class.new(error_data['message'])
|
56
|
+
def create_healing_workspace(class_name, method_name)
|
57
|
+
puts "🏥 Creating isolated healing workspace for #{class_name}##{method_name}"
|
39
58
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
59
|
+
# Create unique workspace
|
60
|
+
workspace_path = CodeHealer::HealingWorkspaceManager.create_healing_workspace(
|
61
|
+
Rails.root.to_s,
|
62
|
+
nil # Use current branch
|
63
|
+
)
|
44
64
|
|
45
|
-
|
65
|
+
puts "✅ Healing workspace created: #{workspace_path}"
|
66
|
+
workspace_path
|
46
67
|
end
|
47
|
-
|
48
|
-
def
|
49
|
-
puts "
|
68
|
+
|
69
|
+
def apply_fixes_in_workspace(workspace_path, error, class_name, method_name)
|
70
|
+
puts "🔧 Applying fixes in isolated workspace"
|
50
71
|
|
51
|
-
#
|
52
|
-
|
72
|
+
# Determine evolution strategy
|
73
|
+
evolution_method = CodeHealer::ConfigManager.evolution_method
|
53
74
|
|
54
|
-
|
55
|
-
|
75
|
+
case evolution_method
|
76
|
+
when 'claude_code_terminal'
|
77
|
+
handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
78
|
+
when 'api'
|
79
|
+
handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
80
|
+
when 'hybrid'
|
81
|
+
handle_hybrid_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
56
82
|
else
|
57
|
-
puts "❌
|
58
|
-
|
83
|
+
puts "❌ Unknown evolution method: #{evolution_method}"
|
84
|
+
false
|
59
85
|
end
|
60
86
|
end
|
61
|
-
|
62
|
-
def
|
63
|
-
puts "
|
87
|
+
|
88
|
+
def handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
89
|
+
puts "🤖 Using Claude Code Terminal for evolution in workspace..."
|
90
|
+
|
91
|
+
# Change to workspace directory for Claude Code operations
|
92
|
+
Dir.chdir(workspace_path) do
|
93
|
+
success = CodeHealer::ClaudeCodeEvolutionHandler.handle_error_with_claude_code(
|
94
|
+
error, class_name, method_name, nil # file_path not needed in workspace
|
95
|
+
)
|
96
|
+
|
97
|
+
if success
|
98
|
+
puts "✅ Claude Code evolution completed successfully in workspace!"
|
99
|
+
true
|
100
|
+
else
|
101
|
+
puts "❌ Claude Code evolution failed in workspace"
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
108
|
+
puts "🌐 Using OpenAI API for evolution in workspace..."
|
64
109
|
|
65
110
|
# Load business context for API evolution
|
66
111
|
business_context = CodeHealer::BusinessContextManager.get_context_for_error(
|
@@ -69,31 +114,56 @@ class EvolutionJob
|
|
69
114
|
|
70
115
|
puts "📋 Business context loaded for API evolution"
|
71
116
|
|
72
|
-
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
117
|
+
# Change to workspace directory for API operations
|
118
|
+
Dir.chdir(workspace_path) do
|
119
|
+
success = CodeHealer::SimpleEvolution.handle_error_with_mcp_intelligence(
|
120
|
+
error, class_name, method_name, nil, business_context # file_path not needed in workspace
|
121
|
+
)
|
122
|
+
|
123
|
+
if success
|
124
|
+
puts "✅ API evolution completed successfully in workspace!"
|
125
|
+
true
|
126
|
+
else
|
127
|
+
puts "❌ API evolution failed in workspace"
|
128
|
+
false
|
129
|
+
end
|
82
130
|
end
|
83
131
|
end
|
84
|
-
|
85
|
-
def
|
86
|
-
puts "🔄 Using Hybrid approach for evolution..."
|
132
|
+
|
133
|
+
def handle_hybrid_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
134
|
+
puts "🔄 Using Hybrid approach for evolution in workspace..."
|
87
135
|
|
88
136
|
begin
|
89
137
|
# Try Claude Code first
|
90
|
-
success =
|
91
|
-
return if success
|
138
|
+
success = handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
139
|
+
return success if success
|
92
140
|
rescue => e
|
93
141
|
puts "⚠️ Claude Code failed, falling back to API: #{e.message}"
|
94
142
|
end
|
95
143
|
|
96
144
|
# Fallback to API
|
97
|
-
|
145
|
+
handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
146
|
+
end
|
147
|
+
|
148
|
+
def cleanup_workspace(workspace_path)
|
149
|
+
return unless workspace_path && Dir.exist?(workspace_path)
|
150
|
+
|
151
|
+
puts "🧹 Cleaning up healing workspace: #{workspace_path}"
|
152
|
+
CodeHealer::HealingWorkspaceManager.cleanup_workspace(workspace_path)
|
153
|
+
end
|
154
|
+
|
155
|
+
def reconstruct_error(error_data)
|
156
|
+
# Reconstruct the error object from serialized data
|
157
|
+
error_class = Object.const_get(error_data['class'])
|
158
|
+
error = error_class.new(error_data['message'])
|
159
|
+
|
160
|
+
# Restore backtrace if available
|
161
|
+
if error_data['backtrace']
|
162
|
+
error.set_backtrace(error_data['backtrace'])
|
163
|
+
end
|
164
|
+
|
165
|
+
error
|
98
166
|
end
|
167
|
+
|
168
|
+
|
99
169
|
end
|
@@ -1,167 +1,187 @@
|
|
1
|
-
|
1
|
+
require 'sidekiq'
|
2
2
|
|
3
3
|
module CodeHealer
|
4
4
|
class HealingJob
|
5
5
|
include Sidekiq::Job
|
6
|
-
|
6
|
+
|
7
7
|
sidekiq_options retry: 3, backtrace: true, queue: 'evolution'
|
8
|
-
|
9
|
-
def perform(
|
10
|
-
puts "🚀
|
8
|
+
|
9
|
+
def perform(*args)
|
10
|
+
puts "🚀 [HEALING_JOB] Starting job with args: #{args.inspect}"
|
11
11
|
|
12
|
-
#
|
13
|
-
error
|
12
|
+
# Support both legacy and new invocation styles
|
13
|
+
error, class_name, method_name, evolution_method, backtrace = parse_args(args)
|
14
14
|
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
puts "🚀 [HEALING_JOB] Parsed args - Error: #{error.class}, Class: #{class_name}, Method: #{method_name}, Evolution: #{evolution_method}"
|
16
|
+
puts "🚀 [HEALING_JOB] Backtrace length: #{backtrace&.length || 0}"
|
17
|
+
|
18
|
+
puts "🚀 Evolution Job Started: #{class_name}##{method_name}"
|
19
|
+
|
20
|
+
puts "🏥 [HEALING_JOB] About to create isolated healing workspace..."
|
21
|
+
# Create isolated healing workspace
|
22
|
+
workspace_path = create_healing_workspace(class_name, method_name)
|
23
|
+
puts "🏥 [HEALING_JOB] Workspace created: #{workspace_path}"
|
24
|
+
|
25
|
+
begin
|
26
|
+
puts "🔧 [HEALING_JOB] About to apply fixes in isolated environment..."
|
27
|
+
# Apply fixes in isolated environment
|
28
|
+
success = apply_fixes_in_workspace(workspace_path, error, class_name, method_name, evolution_method)
|
29
|
+
|
30
|
+
if success
|
31
|
+
# Test fixes in isolated environment
|
32
|
+
test_success = CodeHealer::HealingWorkspaceManager.test_fixes_in_workspace(workspace_path)
|
33
|
+
|
34
|
+
if test_success
|
35
|
+
# Merge back to main repo
|
36
|
+
healing_branch = CodeHealer::HealingWorkspaceManager.merge_fixes_back(
|
37
|
+
Rails.root.to_s,
|
38
|
+
workspace_path,
|
39
|
+
CodeHealer::ConfigManager.git_settings['pr_target_branch'] || 'main'
|
40
|
+
)
|
41
|
+
|
42
|
+
if healing_branch
|
43
|
+
puts "✅ Fixes applied, tested, and merged successfully! Branch: #{healing_branch}"
|
44
|
+
else
|
45
|
+
puts "⚠️ Fixes applied and tested, but merge failed"
|
46
|
+
end
|
47
|
+
else
|
48
|
+
puts "⚠️ Fixes applied but failed tests, not merging back"
|
49
|
+
end
|
50
|
+
else
|
51
|
+
puts "❌ Failed to apply fixes in workspace"
|
52
|
+
end
|
53
|
+
ensure
|
54
|
+
# Clean up workspace
|
55
|
+
cleanup_workspace(workspace_path)
|
25
56
|
end
|
57
|
+
|
58
|
+
puts "✅ Evolution Job Completed: #{class_name}##{method_name}"
|
59
|
+
rescue => e
|
60
|
+
puts "❌ Evolution Job Failed: #{e.message}"
|
61
|
+
puts "📍 Backtrace: #{e.backtrace.first(5)}"
|
62
|
+
raise e # Re-raise to trigger Sidekiq retry
|
26
63
|
end
|
27
|
-
|
64
|
+
|
28
65
|
private
|
29
|
-
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
66
|
+
|
67
|
+
def parse_args(args)
|
68
|
+
# Formats supported:
|
69
|
+
# 1) [error_class, error_message, class_name, method_name, evolution_method, backtrace]
|
70
|
+
# 2) [error_data_hash, class_name, method_name, file_path]
|
71
|
+
if args.length >= 6 && args[0].is_a?(String)
|
72
|
+
error_class, error_message, class_name, method_name, evolution_method, backtrace = args
|
73
|
+
error = reconstruct_error({ 'class' => error_class, 'message' => error_message, 'backtrace' => backtrace })
|
74
|
+
[error, class_name, method_name, evolution_method, backtrace]
|
75
|
+
elsif args.length == 4 && args[0].is_a?(Hash)
|
76
|
+
error_data, class_name, method_name, _file_path = args
|
77
|
+
error = reconstruct_error(error_data)
|
78
|
+
evolution_method = CodeHealer::ConfigManager.evolution_method
|
79
|
+
[error, class_name, method_name, evolution_method, error.backtrace]
|
42
80
|
else
|
43
|
-
|
81
|
+
raise ArgumentError, "Unsupported HealingJob arguments: #{args.inspect}"
|
44
82
|
end
|
45
83
|
end
|
46
|
-
|
47
|
-
def
|
48
|
-
puts "
|
49
|
-
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
84
|
+
|
85
|
+
def create_healing_workspace(class_name, method_name)
|
86
|
+
puts "🏥 Creating isolated healing workspace for #{class_name}##{method_name}"
|
87
|
+
|
88
|
+
# Create unique workspace
|
89
|
+
workspace_path = CodeHealer::HealingWorkspaceManager.create_healing_workspace(
|
90
|
+
Rails.root.to_s,
|
91
|
+
nil # Use current branch
|
92
|
+
)
|
93
|
+
|
94
|
+
puts "✅ Healing workspace created: #{workspace_path}"
|
95
|
+
workspace_path
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply_fixes_in_workspace(workspace_path, error, class_name, method_name, evolution_method)
|
99
|
+
puts "🔧 Applying fixes in isolated workspace"
|
100
|
+
|
101
|
+
case evolution_method
|
102
|
+
when 'claude_code_terminal'
|
103
|
+
handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
104
|
+
when 'api'
|
105
|
+
handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
106
|
+
when 'hybrid'
|
107
|
+
begin
|
108
|
+
success = handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
109
|
+
return true if success
|
110
|
+
rescue => e
|
111
|
+
puts "⚠️ Claude Code failed, falling back to API: #{e.message}"
|
112
|
+
end
|
113
|
+
handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
70
114
|
else
|
71
|
-
puts "
|
115
|
+
puts "❌ Unknown evolution method: #{evolution_method}"
|
116
|
+
false
|
72
117
|
end
|
73
118
|
end
|
74
|
-
|
75
|
-
def
|
76
|
-
puts "
|
77
|
-
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
119
|
+
|
120
|
+
def handle_claude_code_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
121
|
+
puts "🤖 Using Claude Code Terminal for evolution in workspace..."
|
122
|
+
|
123
|
+
# Change to workspace directory for Claude Code operations
|
124
|
+
Dir.chdir(workspace_path) do
|
125
|
+
success = CodeHealer::ClaudeCodeEvolutionHandler.handle_error_with_claude_code(
|
126
|
+
error, class_name, method_name, nil # file_path not needed in workspace
|
127
|
+
)
|
128
|
+
|
129
|
+
if success
|
130
|
+
puts "✅ Claude Code evolution completed successfully in workspace!"
|
131
|
+
true
|
132
|
+
else
|
133
|
+
puts "❌ Claude Code evolution failed in workspace"
|
134
|
+
false
|
135
|
+
end
|
85
136
|
end
|
86
137
|
end
|
87
|
-
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
deeper_app_line = backtrace.find do |line|
|
103
|
-
line.include?('/app/') &&
|
104
|
-
line =~ /in `(.+)'/ &&
|
105
|
-
!core_methods.include?($1) &&
|
106
|
-
!$1.include?('block in') &&
|
107
|
-
!$1.include?('each') &&
|
108
|
-
!$1.include?('map') &&
|
109
|
-
!$1.include?('reduce')
|
110
|
-
end
|
111
|
-
|
112
|
-
if deeper_app_line && deeper_app_line =~ /(.+):(\d+):in `(.+)'/
|
113
|
-
file_path = $1
|
114
|
-
method_name = $3
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
# Handle iterator methods and blocks
|
119
|
-
if method_name && (
|
120
|
-
method_name.include?('block in') ||
|
121
|
-
method_name.include?('each') ||
|
122
|
-
method_name.include?('map') ||
|
123
|
-
method_name.include?('reduce') ||
|
124
|
-
method_name.include?('sum')
|
138
|
+
|
139
|
+
def handle_api_evolution_in_workspace(workspace_path, error, class_name, method_name)
|
140
|
+
puts "🌐 Using OpenAI API for evolution in workspace..."
|
141
|
+
|
142
|
+
# Load business context for API evolution
|
143
|
+
business_context = CodeHealer::BusinessContextManager.get_context_for_error(
|
144
|
+
error, class_name, method_name
|
145
|
+
)
|
146
|
+
|
147
|
+
puts "📋 Business context loaded for API evolution"
|
148
|
+
|
149
|
+
# Change to workspace directory for API operations
|
150
|
+
Dir.chdir(workspace_path) do
|
151
|
+
success = CodeHealer::SimpleEvolution.handle_error_with_mcp_intelligence(
|
152
|
+
error, class_name, method_name, nil, business_context # file_path not needed in workspace
|
125
153
|
)
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
!$1.include?('sum')
|
134
|
-
end
|
135
|
-
|
136
|
-
if containing_line && containing_line =~ /(.+):(\d+):in `(.+)'/
|
137
|
-
file_path = $1
|
138
|
-
method_name = $3
|
139
|
-
end
|
154
|
+
|
155
|
+
if success
|
156
|
+
puts "✅ API evolution completed successfully in workspace!"
|
157
|
+
true
|
158
|
+
else
|
159
|
+
puts "❌ API evolution failed in workspace"
|
160
|
+
false
|
140
161
|
end
|
141
|
-
|
142
|
-
return file_path if file_path
|
143
162
|
end
|
144
|
-
|
145
|
-
nil
|
146
163
|
end
|
147
|
-
|
148
|
-
def
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
164
|
+
|
165
|
+
def cleanup_workspace(workspace_path)
|
166
|
+
return unless workspace_path && Dir.exist?(workspace_path)
|
167
|
+
|
168
|
+
puts "🧹 Cleaning up healing workspace: #{workspace_path}"
|
169
|
+
CodeHealer::HealingWorkspaceManager.cleanup_workspace(workspace_path)
|
170
|
+
end
|
171
|
+
|
172
|
+
def reconstruct_error(error_data)
|
173
|
+
# Reconstruct the error object from serialized data
|
174
|
+
error_class = Object.const_get(error_data['class'])
|
175
|
+
error = error_class.new(error_data['message'])
|
176
|
+
|
177
|
+
# Restore backtrace if available
|
178
|
+
if error_data['backtrace']
|
179
|
+
error.set_backtrace(error_data['backtrace'])
|
159
180
|
end
|
160
|
-
|
181
|
+
|
161
182
|
error
|
162
|
-
rescue
|
163
|
-
# Fallback to generic error
|
164
|
-
StandardError.new(error_message)
|
165
183
|
end
|
166
184
|
end
|
167
185
|
end
|
186
|
+
|
187
|
+
# This duplicate class has been removed to fix the isolated healing workspace system
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module CodeHealer
|
5
|
+
# Manages isolated healing workspaces for safe code evolution
|
6
|
+
class HealingWorkspaceManager
|
7
|
+
class << self
|
8
|
+
def create_healing_workspace(repo_path, branch_name = nil)
|
9
|
+
puts "🏥 [WORKSPACE] Starting workspace creation..."
|
10
|
+
puts "🏥 [WORKSPACE] Repo path: #{repo_path}"
|
11
|
+
puts "🏥 [WORKSPACE] Branch name: #{branch_name || 'current'}"
|
12
|
+
|
13
|
+
config = CodeHealer::ConfigManager.code_heal_directory_config
|
14
|
+
puts "🏥 [WORKSPACE] Raw config: #{config.inspect}"
|
15
|
+
|
16
|
+
base_path = config['path'] || config[:path] || '/tmp/code_healer_workspaces'
|
17
|
+
puts "🏥 [WORKSPACE] Base heal dir: #{base_path}"
|
18
|
+
|
19
|
+
# Create unique workspace directory
|
20
|
+
workspace_id = "healing_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
|
21
|
+
workspace_path = File.join(base_path, workspace_id)
|
22
|
+
|
23
|
+
puts "🏥 [WORKSPACE] Workspace ID: #{workspace_id}"
|
24
|
+
puts "🏥 [WORKSPACE] Full workspace path: #{workspace_path}"
|
25
|
+
|
26
|
+
begin
|
27
|
+
puts "🏥 [WORKSPACE] Creating base directory..."
|
28
|
+
# Ensure code heal directory exists
|
29
|
+
FileUtils.mkdir_p(base_path)
|
30
|
+
puts "🏥 [WORKSPACE] Base directory created/verified: #{base_path}"
|
31
|
+
|
32
|
+
# Clone current branch to workspace
|
33
|
+
strategy = clone_strategy
|
34
|
+
puts "🏥 [WORKSPACE] Clone strategy: #{strategy}"
|
35
|
+
|
36
|
+
if strategy == "branch"
|
37
|
+
puts "🏥 [WORKSPACE] Using branch-only cloning..."
|
38
|
+
clone_current_branch(repo_path, workspace_path, branch_name)
|
39
|
+
else
|
40
|
+
puts "🏥 [WORKSPACE] Using full repo cloning..."
|
41
|
+
clone_full_repo(repo_path, workspace_path, branch_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "🏥 [WORKSPACE] Workspace creation completed successfully!"
|
45
|
+
puts "🏥 [WORKSPACE] Final workspace path: #{workspace_path}"
|
46
|
+
puts "🏥 [WORKSPACE] Workspace contents: #{Dir.entries(workspace_path).join(', ')}"
|
47
|
+
workspace_path
|
48
|
+
rescue => e
|
49
|
+
puts "❌ Failed to create healing workspace: #{e.message}"
|
50
|
+
cleanup_workspace(workspace_path) if Dir.exist?(workspace_path)
|
51
|
+
raise e
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def apply_fixes_in_workspace(workspace_path, fixes, class_name, method_name)
|
56
|
+
puts "🔧 [WORKSPACE] Starting fix application..."
|
57
|
+
puts "🔧 [WORKSPACE] Workspace: #{workspace_path}"
|
58
|
+
puts "🔧 [WORKSPACE] Class: #{class_name}, Method: #{method_name}"
|
59
|
+
puts "🔧 [WORKSPACE] Fixes to apply: #{fixes.inspect}"
|
60
|
+
|
61
|
+
begin
|
62
|
+
puts "🔧 [WORKSPACE] Processing #{fixes.length} fixes..."
|
63
|
+
# Apply each fix to the workspace
|
64
|
+
fixes.each_with_index do |fix, index|
|
65
|
+
puts "🔧 [WORKSPACE] Processing fix #{index + 1}: #{fix.inspect}"
|
66
|
+
file_path = File.join(workspace_path, fix[:file_path])
|
67
|
+
puts "🔧 [WORKSPACE] Target file: #{file_path}"
|
68
|
+
puts "🔧 [WORKSPACE] File exists: #{File.exist?(file_path)}"
|
69
|
+
|
70
|
+
next unless File.exist?(file_path)
|
71
|
+
|
72
|
+
puts "🔧 [WORKSPACE] Creating backup..."
|
73
|
+
# Backup original file
|
74
|
+
backup_file(file_path)
|
75
|
+
|
76
|
+
puts "🔧 [WORKSPACE] Applying fix to file..."
|
77
|
+
# Apply the fix
|
78
|
+
apply_fix_to_file(file_path, fix[:new_code], class_name, method_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
puts "✅ Fixes applied successfully in workspace"
|
82
|
+
true
|
83
|
+
rescue => e
|
84
|
+
puts "❌ Failed to apply fixes in workspace: #{e.message}"
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_fixes_in_workspace(workspace_path)
|
90
|
+
config = CodeHealer::ConfigManager.code_heal_directory_config
|
91
|
+
|
92
|
+
puts "🧪 Testing fixes in workspace: #{workspace_path}"
|
93
|
+
|
94
|
+
begin
|
95
|
+
# Change to workspace directory
|
96
|
+
Dir.chdir(workspace_path) do
|
97
|
+
# Run basic syntax check
|
98
|
+
syntax_check = system("ruby -c #{find_ruby_files.join(' ')} 2>/dev/null")
|
99
|
+
return false unless syntax_check
|
100
|
+
|
101
|
+
# Run tests if available
|
102
|
+
if File.exist?('Gemfile')
|
103
|
+
bundle_check = system("bundle check >/dev/null 2>&1")
|
104
|
+
return false unless bundle_check
|
105
|
+
|
106
|
+
# Run tests if RSpec is available
|
107
|
+
if File.exist?('spec') || File.exist?('test')
|
108
|
+
test_result = system("bundle exec rspec --dry-run >/dev/null 2>&1") ||
|
109
|
+
system("bundle exec rake test:prepare >/dev/null 2>&1")
|
110
|
+
puts "🧪 Test preparation: #{test_result ? '✅' : '⚠️'}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
puts "✅ Workspace validation passed"
|
115
|
+
true
|
116
|
+
end
|
117
|
+
rescue => e
|
118
|
+
puts "❌ Workspace validation failed: #{e.message}"
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def merge_fixes_back(repo_path, workspace_path, branch_name)
|
124
|
+
puts "🔄 Merging fixes back to main repository"
|
125
|
+
|
126
|
+
begin
|
127
|
+
# Create healing branch in main repo
|
128
|
+
Dir.chdir(repo_path) do
|
129
|
+
# Ensure we're on the target branch
|
130
|
+
system("git checkout #{branch_name}")
|
131
|
+
system("git pull origin #{branch_name}")
|
132
|
+
|
133
|
+
# Create healing branch
|
134
|
+
healing_branch = "code-healer-fix-#{Time.now.to_i}"
|
135
|
+
system("git checkout -b #{healing_branch}")
|
136
|
+
|
137
|
+
# Copy fixed files from workspace
|
138
|
+
copy_fixed_files(workspace_path, repo_path)
|
139
|
+
|
140
|
+
# Commit changes
|
141
|
+
system("git add .")
|
142
|
+
commit_message = "Fix applied by CodeHealer: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
143
|
+
system("git commit -m '#{commit_message}'")
|
144
|
+
|
145
|
+
# Push branch
|
146
|
+
system("git push origin #{healing_branch}")
|
147
|
+
|
148
|
+
puts "✅ Healing branch created: #{healing_branch}"
|
149
|
+
healing_branch
|
150
|
+
end
|
151
|
+
rescue => e
|
152
|
+
puts "❌ Failed to merge fixes back: #{e.message}"
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def cleanup_workspace(workspace_path)
|
158
|
+
puts "🧹 [WORKSPACE] Starting workspace cleanup..."
|
159
|
+
puts "🧹 [WORKSPACE] Target: #{workspace_path}"
|
160
|
+
puts "🧹 [WORKSPACE] Exists: #{Dir.exist?(workspace_path)}"
|
161
|
+
|
162
|
+
return unless Dir.exist?(workspace_path)
|
163
|
+
|
164
|
+
puts "🧹 [WORKSPACE] Removing workspace directory..."
|
165
|
+
FileUtils.rm_rf(workspace_path)
|
166
|
+
puts "🧹 [WORKSPACE] Workspace cleanup completed"
|
167
|
+
puts "🧹 [WORKSPACE] Directory still exists: #{Dir.exist?(workspace_path)}"
|
168
|
+
end
|
169
|
+
|
170
|
+
def cleanup_expired_workspaces
|
171
|
+
config = CodeHealer::ConfigManager.code_heal_directory_config
|
172
|
+
auto_cleanup = config['auto_cleanup']
|
173
|
+
auto_cleanup = config[:auto_cleanup] if auto_cleanup.nil?
|
174
|
+
return unless auto_cleanup
|
175
|
+
|
176
|
+
puts "🧹 Cleaning up expired healing workspaces"
|
177
|
+
|
178
|
+
base_path = config['path'] || config[:path] || '/tmp/code_healer_workspaces'
|
179
|
+
Dir.glob(File.join(base_path, "healing_*")).each do |workspace_path|
|
180
|
+
next unless Dir.exist?(workspace_path)
|
181
|
+
|
182
|
+
# Check if workspace is expired
|
183
|
+
hours = config['cleanup_after_hours'] || config[:cleanup_after_hours]
|
184
|
+
hours = hours.to_i if hours
|
185
|
+
if workspace_expired?(workspace_path, hours)
|
186
|
+
cleanup_workspace(workspace_path)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def clone_strategy
|
194
|
+
cfg = CodeHealer::ConfigManager.code_heal_directory_config
|
195
|
+
cfg['clone_strategy'] || cfg[:clone_strategy] || "branch"
|
196
|
+
end
|
197
|
+
|
198
|
+
def clone_current_branch(repo_path, workspace_path, branch_name)
|
199
|
+
puts "🌿 [WORKSPACE] Starting branch cloning..."
|
200
|
+
Dir.chdir(repo_path) do
|
201
|
+
current_branch = branch_name || `git branch --show-current`.strip
|
202
|
+
puts "🌿 [WORKSPACE] Current branch: #{current_branch}"
|
203
|
+
puts "🌿 [WORKSPACE] Executing: git clone --single-branch --branch #{current_branch} #{repo_path} #{workspace_path}"
|
204
|
+
|
205
|
+
# Clone only the current branch
|
206
|
+
result = system("git clone --single-branch --branch #{current_branch} #{repo_path} #{workspace_path}")
|
207
|
+
puts "🌿 [WORKSPACE] Clone result: #{result ? 'SUCCESS' : 'FAILED'}"
|
208
|
+
|
209
|
+
if result
|
210
|
+
puts "🌿 [WORKSPACE] Removing .git to avoid conflicts..."
|
211
|
+
# Remove .git to avoid conflicts
|
212
|
+
FileUtils.rm_rf(File.join(workspace_path, '.git'))
|
213
|
+
puts "🌿 [WORKSPACE] .git removed successfully"
|
214
|
+
else
|
215
|
+
puts "🌿 [WORKSPACE] Clone failed, checking workspace..."
|
216
|
+
puts "🌿 [WORKSPACE] Workspace exists: #{Dir.exist?(workspace_path)}"
|
217
|
+
puts "🌿 [WORKSPACE] Workspace contents: #{Dir.exist?(workspace_path) ? Dir.entries(workspace_path).join(', ') : 'N/A'}"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def clone_full_repo(repo_path, workspace_path, branch_name)
|
223
|
+
puts "🌿 [WORKSPACE] Starting full repo cloning..."
|
224
|
+
Dir.chdir(repo_path) do
|
225
|
+
current_branch = branch_name || `git branch --show-current`.strip
|
226
|
+
puts "🌿 [WORKSPACE] Target branch: #{current_branch}"
|
227
|
+
puts "🌿 [WORKSPACE] Executing: git clone #{repo_path} #{workspace_path}"
|
228
|
+
|
229
|
+
# Clone full repo
|
230
|
+
result = system("git clone #{repo_path} #{workspace_path}")
|
231
|
+
puts "🌿 [WORKSPACE] Clone result: #{result ? 'SUCCESS' : 'FAILED'}"
|
232
|
+
|
233
|
+
if result
|
234
|
+
puts "🌿 [WORKSPACE] Switching to branch: #{current_branch}"
|
235
|
+
# Switch to specific branch
|
236
|
+
Dir.chdir(workspace_path) do
|
237
|
+
checkout_result = system("git checkout #{current_branch}")
|
238
|
+
puts "🌿 [WORKSPACE] Checkout result: #{checkout_result ? 'SUCCESS' : 'FAILED'}"
|
239
|
+
end
|
240
|
+
else
|
241
|
+
puts "🌿 [WORKSPACE] Full repo clone failed"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def backup_file(file_path)
|
247
|
+
backup_path = "#{file_path}.code_healer_backup"
|
248
|
+
FileUtils.cp(file_path, backup_path)
|
249
|
+
end
|
250
|
+
|
251
|
+
def apply_fix_to_file(file_path, new_code, class_name, method_name)
|
252
|
+
content = File.read(file_path)
|
253
|
+
|
254
|
+
# Find and replace the method
|
255
|
+
method_pattern = /def\s+#{Regexp.escape(method_name)}\s*\([^)]*\)(.*?)end/m
|
256
|
+
if content.match(method_pattern)
|
257
|
+
content.gsub!(method_pattern, new_code)
|
258
|
+
File.write(file_path, content)
|
259
|
+
puts "✅ Applied fix to #{File.basename(file_path)}##{method_name}"
|
260
|
+
else
|
261
|
+
puts "⚠️ Could not find method #{method_name} in #{File.basename(file_path)}"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def find_ruby_files
|
266
|
+
Dir.glob("**/*.rb")
|
267
|
+
end
|
268
|
+
|
269
|
+
def copy_fixed_files(workspace_path, repo_path)
|
270
|
+
# Copy all Ruby files from workspace to repo
|
271
|
+
Dir.glob(File.join(workspace_path, "**/*.rb")).each do |workspace_file|
|
272
|
+
relative_path = workspace_file.sub(workspace_path + "/", "")
|
273
|
+
repo_file = File.join(repo_path, relative_path)
|
274
|
+
|
275
|
+
if File.exist?(repo_file)
|
276
|
+
FileUtils.cp(workspace_file, repo_file)
|
277
|
+
puts "📁 Copied fixed file: #{relative_path}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def workspace_expired?(workspace_path, hours)
|
283
|
+
return false unless hours && hours > 0
|
284
|
+
|
285
|
+
# Extract timestamp from workspace name
|
286
|
+
if workspace_path =~ /healing_(\d+)/
|
287
|
+
timestamp = $1.to_i
|
288
|
+
age_hours = (Time.now.to_i - timestamp) / 3600
|
289
|
+
age_hours > hours
|
290
|
+
else
|
291
|
+
false
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
data/lib/code_healer/setup.rb
CHANGED
@@ -153,6 +153,17 @@ puts "🌿 Git Branch Configuration:"
|
|
153
153
|
branch_prefix = ask_for_input("Enter branch prefix for healing branches (default: evolve):", default: "evolve")
|
154
154
|
pr_target_branch = ask_for_input("Enter target branch for pull requests (default: main):", default: "main")
|
155
155
|
|
156
|
+
# Code Heal Directory Configuration
|
157
|
+
puts
|
158
|
+
puts "🏥 Code Heal Directory Configuration:"
|
159
|
+
puts "This directory will store isolated copies of your code for safe healing."
|
160
|
+
puts "CodeHealer will clone your current branch here before making fixes."
|
161
|
+
puts
|
162
|
+
|
163
|
+
code_heal_directory = ask_for_input("Enter code heal directory path (default: /tmp/code_healer_workspaces):", default: "/tmp/code_healer_workspaces")
|
164
|
+
auto_cleanup = ask_for_yes_no("Automatically clean up healing workspaces after use?", default: true)
|
165
|
+
cleanup_after_hours = ask_for_input("Clean up workspaces after how many hours? (default: 24):", default: "24")
|
166
|
+
|
156
167
|
# Business Context
|
157
168
|
puts
|
158
169
|
puts "💼 Business Context Setup:"
|
@@ -297,6 +308,14 @@ config_content = <<~YAML
|
|
297
308
|
max_concurrent_healing: 3
|
298
309
|
healing_timeout: 300
|
299
310
|
retry_attempts: 3
|
311
|
+
|
312
|
+
# Code Heal Directory Configuration
|
313
|
+
code_heal_directory:
|
314
|
+
path: "#{code_heal_directory}"
|
315
|
+
auto_cleanup: #{auto_cleanup}
|
316
|
+
cleanup_after_hours: #{cleanup_after_hours}
|
317
|
+
max_workspaces: 10
|
318
|
+
clone_strategy: "branch" # Options: branch, full_repo
|
300
319
|
YAML
|
301
320
|
|
302
321
|
create_file_with_content('config/code_healer.yml', config_content, dry_run: options[:dry_run])
|
@@ -366,14 +385,19 @@ else
|
|
366
385
|
puts "4. Start your Rails server: rails s"
|
367
386
|
puts
|
368
387
|
puts "🔒 Security Notes:"
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
388
|
+
puts " - .env file contains your actual API keys and is ignored by git"
|
389
|
+
puts " - .env.example is safe to commit and shows the required format"
|
390
|
+
puts " - Never commit .env files with real secrets to version control"
|
391
|
+
puts
|
392
|
+
puts "🏥 Code Heal Directory:"
|
393
|
+
puts " - Your code will be cloned to: #{code_heal_directory}"
|
394
|
+
puts " - This ensures safe, isolated healing without affecting your running server"
|
395
|
+
puts " - Workspaces are automatically cleaned up after #{cleanup_after_hours} hours"
|
396
|
+
puts
|
397
|
+
puts "⚙️ Configuration:"
|
398
|
+
puts " - code_healer.yml contains comprehensive settings with sensible defaults"
|
399
|
+
puts " - Customize the configuration file as needed for your project"
|
400
|
+
puts " - All features are pre-configured and ready to use"
|
377
401
|
puts
|
378
402
|
puts "🌍 Environment Variables:"
|
379
403
|
puts " - Add 'gem \"dotenv-rails\"' to your Gemfile for automatic .env loading"
|
data/lib/code_healer/version.rb
CHANGED
data/lib/code_healer.rb
CHANGED
@@ -21,6 +21,7 @@ autoload :BusinessContextManager, "code_healer/business_context_manager"
|
|
21
21
|
autoload :ClaudeCodeEvolutionHandler, "code_healer/claude_code_evolution_handler"
|
22
22
|
autoload :SimpleHealer, "code_healer/simple_healer"
|
23
23
|
autoload :HealingJob, "code_healer/healing_job"
|
24
|
+
autoload :HealingWorkspaceManager, "code_healer/healing_workspace_manager"
|
24
25
|
autoload :PullRequestCreator, "code_healer/pull_request_creator"
|
25
26
|
autoload :McpServer, "code_healer/mcp_server"
|
26
27
|
autoload :McpTools, "code_healer/mcp_tools"
|
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.5
|
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-08-
|
11
|
+
date: 2025-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -368,6 +368,7 @@ files:
|
|
368
368
|
- lib/code_healer/evolution_job.rb
|
369
369
|
- lib/code_healer/global_handler.rb
|
370
370
|
- lib/code_healer/healing_job.rb
|
371
|
+
- lib/code_healer/healing_workspace_manager.rb
|
371
372
|
- lib/code_healer/mcp.rb
|
372
373
|
- lib/code_healer/mcp_prompts.rb
|
373
374
|
- lib/code_healer/mcp_server.rb
|