enhance_swarm 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
- data/.enhance_swarm/user_patterns.json +37 -0
- data/CHANGELOG.md +184 -0
- data/LICENSE +21 -0
- data/PRODUCTION_TEST_LOG.md +502 -0
- data/README.md +905 -0
- data/Rakefile +28 -0
- data/USAGE_EXAMPLES.md +477 -0
- data/examples/enhance_workflow.md +346 -0
- data/examples/rails_project.md +253 -0
- data/exe/enhance-swarm +30 -0
- data/lib/enhance_swarm/additional_commands.rb +299 -0
- data/lib/enhance_swarm/agent_communicator.rb +460 -0
- data/lib/enhance_swarm/agent_reviewer.rb +283 -0
- data/lib/enhance_swarm/agent_spawner.rb +462 -0
- data/lib/enhance_swarm/cleanup_manager.rb +245 -0
- data/lib/enhance_swarm/cli.rb +1592 -0
- data/lib/enhance_swarm/command_executor.rb +78 -0
- data/lib/enhance_swarm/configuration.rb +324 -0
- data/lib/enhance_swarm/control_agent.rb +307 -0
- data/lib/enhance_swarm/dependency_validator.rb +195 -0
- data/lib/enhance_swarm/error_recovery.rb +785 -0
- data/lib/enhance_swarm/generator.rb +194 -0
- data/lib/enhance_swarm/interrupt_handler.rb +512 -0
- data/lib/enhance_swarm/logger.rb +106 -0
- data/lib/enhance_swarm/mcp_integration.rb +85 -0
- data/lib/enhance_swarm/monitor.rb +28 -0
- data/lib/enhance_swarm/notification_manager.rb +444 -0
- data/lib/enhance_swarm/orchestrator.rb +313 -0
- data/lib/enhance_swarm/output_streamer.rb +281 -0
- data/lib/enhance_swarm/process_monitor.rb +266 -0
- data/lib/enhance_swarm/progress_tracker.rb +215 -0
- data/lib/enhance_swarm/project_analyzer.rb +612 -0
- data/lib/enhance_swarm/resource_manager.rb +177 -0
- data/lib/enhance_swarm/retry_handler.rb +40 -0
- data/lib/enhance_swarm/session_manager.rb +247 -0
- data/lib/enhance_swarm/signal_handler.rb +95 -0
- data/lib/enhance_swarm/smart_defaults.rb +708 -0
- data/lib/enhance_swarm/task_integration.rb +150 -0
- data/lib/enhance_swarm/task_manager.rb +174 -0
- data/lib/enhance_swarm/version.rb +5 -0
- data/lib/enhance_swarm/visual_dashboard.rb +555 -0
- data/lib/enhance_swarm/web_ui.rb +211 -0
- data/lib/enhance_swarm.rb +69 -0
- data/setup.sh +86 -0
- data/sig/enhance_swarm.rbs +4 -0
- data/templates/claude/CLAUDE.md +160 -0
- data/templates/claude/MCP.md +117 -0
- data/templates/claude/PERSONAS.md +114 -0
- data/templates/claude/RULES.md +221 -0
- data/test_builtin_functionality.rb +121 -0
- data/test_core_components.rb +156 -0
- data/test_real_claude_integration.rb +285 -0
- data/test_security.rb +150 -0
- data/test_smart_defaults.rb +155 -0
- data/test_task_integration.rb +173 -0
- data/test_web_ui.rb +245 -0
- data/web/assets/css/main.css +645 -0
- data/web/assets/js/kanban.js +499 -0
- data/web/assets/js/main.js +525 -0
- data/web/templates/dashboard.html.erb +226 -0
- data/web/templates/kanban.html.erb +193 -0
- metadata +293 -0
@@ -0,0 +1,785 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'json'
|
5
|
+
require 'digest'
|
6
|
+
require 'timeout'
|
7
|
+
|
8
|
+
module EnhanceSwarm
|
9
|
+
class ErrorRecovery
|
10
|
+
include Singleton
|
11
|
+
|
12
|
+
RECOVERY_STRATEGIES_FILE = '.enhance_swarm/error_recovery_strategies.json'
|
13
|
+
ERROR_PATTERNS_FILE = '.enhance_swarm/error_patterns.json'
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
ensure_recovery_directory
|
17
|
+
@recovery_strategies = load_recovery_strategies
|
18
|
+
@error_patterns = load_error_patterns
|
19
|
+
@recovery_history = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Analyze error and suggest recovery actions
|
23
|
+
def analyze_error(error, context = {})
|
24
|
+
error_info = {
|
25
|
+
message: error.message,
|
26
|
+
type: error.class.name,
|
27
|
+
context: context,
|
28
|
+
timestamp: Time.now.iso8601
|
29
|
+
}
|
30
|
+
|
31
|
+
# Find matching patterns
|
32
|
+
matching_patterns = find_matching_patterns(error_info)
|
33
|
+
|
34
|
+
# Generate recovery suggestions
|
35
|
+
suggestions = generate_recovery_suggestions(error_info, matching_patterns)
|
36
|
+
|
37
|
+
# Log error for pattern learning
|
38
|
+
log_error_occurrence(error_info)
|
39
|
+
|
40
|
+
{
|
41
|
+
error: error_info,
|
42
|
+
patterns: matching_patterns,
|
43
|
+
suggestions: suggestions,
|
44
|
+
auto_recoverable: auto_recoverable?(error_info, matching_patterns)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Attempt automatic recovery
|
49
|
+
def attempt_recovery(error_analysis, agent_context = {})
|
50
|
+
return false unless error_analysis[:auto_recoverable]
|
51
|
+
|
52
|
+
recovery_attempts = []
|
53
|
+
|
54
|
+
error_analysis[:suggestions].each do |suggestion|
|
55
|
+
auto_executable = suggestion['auto_executable'] || suggestion[:auto_executable]
|
56
|
+
next unless auto_executable
|
57
|
+
|
58
|
+
begin
|
59
|
+
description = suggestion['description'] || suggestion[:description]
|
60
|
+
Logger.info("Attempting automatic recovery: #{description}")
|
61
|
+
|
62
|
+
result = execute_recovery_action(suggestion, agent_context)
|
63
|
+
|
64
|
+
recovery_attempts << {
|
65
|
+
suggestion: suggestion,
|
66
|
+
result: result,
|
67
|
+
success: result[:success],
|
68
|
+
timestamp: Time.now.iso8601
|
69
|
+
}
|
70
|
+
|
71
|
+
# If recovery succeeds, stop trying other strategies
|
72
|
+
if result[:success]
|
73
|
+
log_successful_recovery(error_analysis[:error], suggestion)
|
74
|
+
return result
|
75
|
+
end
|
76
|
+
|
77
|
+
rescue StandardError => recovery_error
|
78
|
+
Logger.error("Recovery attempt failed: #{recovery_error.message}")
|
79
|
+
recovery_attempts << {
|
80
|
+
suggestion: suggestion,
|
81
|
+
result: { success: false, error: recovery_error.message },
|
82
|
+
success: false,
|
83
|
+
timestamp: Time.now.iso8601
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Log all recovery attempts for learning
|
89
|
+
log_recovery_attempts(error_analysis[:error], recovery_attempts)
|
90
|
+
|
91
|
+
{ success: false, attempts: recovery_attempts }
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get human-readable error explanation
|
95
|
+
def explain_error(error, context = {})
|
96
|
+
error_info = {
|
97
|
+
message: error.message,
|
98
|
+
type: error.class.name,
|
99
|
+
context: context
|
100
|
+
}
|
101
|
+
|
102
|
+
matching_patterns = find_matching_patterns(error_info)
|
103
|
+
|
104
|
+
if matching_patterns.any?
|
105
|
+
primary_pattern = matching_patterns.first
|
106
|
+
{
|
107
|
+
explanation: primary_pattern[:explanation],
|
108
|
+
likely_cause: primary_pattern[:likely_cause],
|
109
|
+
prevention_tips: primary_pattern[:prevention_tips] || []
|
110
|
+
}
|
111
|
+
else
|
112
|
+
generate_generic_explanation(error_info)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Learn from successful manual recovery
|
117
|
+
def learn_from_manual_recovery(error, recovery_steps, context = {})
|
118
|
+
error_info = {
|
119
|
+
message: error.message,
|
120
|
+
type: error.class.name,
|
121
|
+
context: context,
|
122
|
+
timestamp: Time.now.iso8601
|
123
|
+
}
|
124
|
+
|
125
|
+
# Create or update pattern
|
126
|
+
pattern_key = generate_pattern_key(error_info)
|
127
|
+
|
128
|
+
@error_patterns[pattern_key] ||= {
|
129
|
+
'error_signatures' => [],
|
130
|
+
'successful_recoveries' => [],
|
131
|
+
'failure_rate' => 0.0,
|
132
|
+
'last_seen' => nil
|
133
|
+
}
|
134
|
+
|
135
|
+
pattern = @error_patterns[pattern_key]
|
136
|
+
|
137
|
+
# Add error signature if not already present
|
138
|
+
signature = extract_error_signature(error_info)
|
139
|
+
unless pattern['error_signatures'].any? { |sig| sig['message_pattern'] == signature[:message_pattern] }
|
140
|
+
pattern['error_signatures'] << signature
|
141
|
+
end
|
142
|
+
|
143
|
+
# Add successful recovery
|
144
|
+
pattern['successful_recoveries'] << {
|
145
|
+
'steps' => recovery_steps,
|
146
|
+
'context' => context,
|
147
|
+
'timestamp' => Time.now.iso8601
|
148
|
+
}
|
149
|
+
|
150
|
+
pattern['last_seen'] = Time.now.iso8601
|
151
|
+
|
152
|
+
save_error_patterns
|
153
|
+
|
154
|
+
Logger.info("Learned new recovery pattern for #{error.class.name}")
|
155
|
+
end
|
156
|
+
|
157
|
+
# Get recovery statistics
|
158
|
+
def recovery_statistics
|
159
|
+
total_errors = @recovery_history.count
|
160
|
+
successful_recoveries = @recovery_history.count { |h| h[:recovery_successful] }
|
161
|
+
|
162
|
+
{
|
163
|
+
total_errors_processed: total_errors,
|
164
|
+
successful_automatic_recoveries: successful_recoveries,
|
165
|
+
recovery_success_rate: total_errors > 0 ? (successful_recoveries.to_f / total_errors * 100).round(1) : 0.0,
|
166
|
+
most_common_errors: most_common_error_types,
|
167
|
+
recovery_patterns_learned: @error_patterns.count
|
168
|
+
}
|
169
|
+
end
|
170
|
+
|
171
|
+
# Clear old recovery data
|
172
|
+
def cleanup_old_data(days_to_keep = 30)
|
173
|
+
cutoff_time = Time.now - (days_to_keep * 24 * 60 * 60)
|
174
|
+
|
175
|
+
# Clean recovery history
|
176
|
+
@recovery_history.reject! { |h| Time.parse(h[:timestamp]) < cutoff_time }
|
177
|
+
|
178
|
+
# Clean old error patterns that haven't been seen recently
|
179
|
+
@error_patterns.reject! do |_, pattern|
|
180
|
+
last_seen = pattern['last_seen'] || pattern[:last_seen]
|
181
|
+
last_seen && Time.parse(last_seen) < cutoff_time
|
182
|
+
end
|
183
|
+
|
184
|
+
save_error_patterns
|
185
|
+
|
186
|
+
Logger.info("Cleaned up error recovery data older than #{days_to_keep} days")
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def load_recovery_strategies
|
192
|
+
return default_recovery_strategies unless File.exist?(RECOVERY_STRATEGIES_FILE)
|
193
|
+
|
194
|
+
JSON.parse(File.read(RECOVERY_STRATEGIES_FILE))
|
195
|
+
rescue StandardError
|
196
|
+
default_recovery_strategies
|
197
|
+
end
|
198
|
+
|
199
|
+
def load_error_patterns
|
200
|
+
return {} unless File.exist?(ERROR_PATTERNS_FILE)
|
201
|
+
|
202
|
+
JSON.parse(File.read(ERROR_PATTERNS_FILE))
|
203
|
+
rescue StandardError
|
204
|
+
{}
|
205
|
+
end
|
206
|
+
|
207
|
+
def save_error_patterns
|
208
|
+
ensure_recovery_directory
|
209
|
+
File.write(ERROR_PATTERNS_FILE, JSON.pretty_generate(@error_patterns))
|
210
|
+
end
|
211
|
+
|
212
|
+
def ensure_recovery_directory
|
213
|
+
dir = File.dirname(RECOVERY_STRATEGIES_FILE)
|
214
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
215
|
+
end
|
216
|
+
|
217
|
+
def default_recovery_strategies
|
218
|
+
{
|
219
|
+
'network_errors' => [
|
220
|
+
{
|
221
|
+
'description' => 'Retry with exponential backoff',
|
222
|
+
'auto_executable' => true,
|
223
|
+
'action' => 'retry_with_backoff',
|
224
|
+
'max_attempts' => 3,
|
225
|
+
'base_delay' => 1
|
226
|
+
},
|
227
|
+
{
|
228
|
+
'description' => 'Check network connectivity',
|
229
|
+
'auto_executable' => false,
|
230
|
+
'action' => 'check_network',
|
231
|
+
'command' => 'ping -c 1 8.8.8.8'
|
232
|
+
}
|
233
|
+
],
|
234
|
+
'file_not_found' => [
|
235
|
+
{
|
236
|
+
'description' => 'Create missing file with default content',
|
237
|
+
'auto_executable' => true,
|
238
|
+
'action' => 'create_default_file'
|
239
|
+
},
|
240
|
+
{
|
241
|
+
'description' => 'Search for similar files in project',
|
242
|
+
'auto_executable' => true,
|
243
|
+
'action' => 'find_similar_files'
|
244
|
+
}
|
245
|
+
],
|
246
|
+
'permission_denied' => [
|
247
|
+
{
|
248
|
+
'description' => 'Fix file permissions',
|
249
|
+
'auto_executable' => false,
|
250
|
+
'action' => 'fix_permissions',
|
251
|
+
'command' => 'chmod +x {file_path}'
|
252
|
+
},
|
253
|
+
{
|
254
|
+
'description' => 'Run with elevated privileges',
|
255
|
+
'auto_executable' => false,
|
256
|
+
'action' => 'elevate_privileges'
|
257
|
+
}
|
258
|
+
],
|
259
|
+
'dependency_missing' => [
|
260
|
+
{
|
261
|
+
'description' => 'Install missing dependencies',
|
262
|
+
'auto_executable' => true,
|
263
|
+
'action' => 'install_dependencies'
|
264
|
+
},
|
265
|
+
{
|
266
|
+
'description' => 'Update package manager',
|
267
|
+
'auto_executable' => false,
|
268
|
+
'action' => 'update_package_manager'
|
269
|
+
}
|
270
|
+
],
|
271
|
+
'timeout_error' => [
|
272
|
+
{
|
273
|
+
'description' => 'Increase timeout and retry',
|
274
|
+
'auto_executable' => true,
|
275
|
+
'action' => 'retry_with_longer_timeout',
|
276
|
+
'timeout_multiplier' => 2.0
|
277
|
+
},
|
278
|
+
{
|
279
|
+
'description' => 'Break task into smaller chunks',
|
280
|
+
'auto_executable' => false,
|
281
|
+
'action' => 'split_task'
|
282
|
+
}
|
283
|
+
],
|
284
|
+
'memory_error' => [
|
285
|
+
{
|
286
|
+
'description' => 'Reduce memory usage and retry',
|
287
|
+
'auto_executable' => true,
|
288
|
+
'action' => 'retry_with_reduced_memory'
|
289
|
+
},
|
290
|
+
{
|
291
|
+
'description' => 'Clear system memory cache',
|
292
|
+
'auto_executable' => false,
|
293
|
+
'action' => 'clear_memory_cache'
|
294
|
+
}
|
295
|
+
]
|
296
|
+
}
|
297
|
+
end
|
298
|
+
|
299
|
+
def find_matching_patterns(error_info)
|
300
|
+
matches = []
|
301
|
+
|
302
|
+
@error_patterns.each do |pattern_key, pattern|
|
303
|
+
confidence = calculate_pattern_match_confidence(error_info, pattern)
|
304
|
+
|
305
|
+
if confidence > 0.5 # 50% confidence threshold
|
306
|
+
matches << {
|
307
|
+
pattern_key: pattern_key,
|
308
|
+
confidence: confidence,
|
309
|
+
explanation: pattern['explanation'] || pattern[:explanation] || "Similar error pattern detected",
|
310
|
+
likely_cause: pattern['likely_cause'] || pattern[:likely_cause] || "Unknown cause",
|
311
|
+
prevention_tips: pattern['prevention_tips'] || pattern[:prevention_tips] || [],
|
312
|
+
successful_recoveries: pattern['successful_recoveries'] || pattern[:successful_recoveries] || []
|
313
|
+
}
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Sort by confidence
|
318
|
+
matches.sort_by { |m| -m[:confidence] }
|
319
|
+
end
|
320
|
+
|
321
|
+
def calculate_pattern_match_confidence(error_info, pattern)
|
322
|
+
error_signatures = pattern['error_signatures'] || pattern[:error_signatures]
|
323
|
+
return 0.0 if error_signatures.nil? || error_signatures.empty?
|
324
|
+
|
325
|
+
max_confidence = 0.0
|
326
|
+
|
327
|
+
error_signatures.each do |signature|
|
328
|
+
confidence = 0.0
|
329
|
+
|
330
|
+
# Match error type
|
331
|
+
error_type = signature['error_type'] || signature[:error_type]
|
332
|
+
if error_type == error_info[:type]
|
333
|
+
confidence += 0.4
|
334
|
+
end
|
335
|
+
|
336
|
+
# Match message patterns
|
337
|
+
message_pattern = signature['message_pattern'] || signature[:message_pattern]
|
338
|
+
if message_pattern && error_info[:message].downcase.include?(message_pattern.downcase)
|
339
|
+
confidence += 0.3
|
340
|
+
end
|
341
|
+
|
342
|
+
# Match context patterns
|
343
|
+
context_patterns = signature['context_patterns'] || signature[:context_patterns]
|
344
|
+
if context_patterns && context_patterns.any?
|
345
|
+
context_matches = context_patterns.count do |pattern|
|
346
|
+
error_info[:context].to_s.downcase.include?(pattern.downcase)
|
347
|
+
end
|
348
|
+
|
349
|
+
confidence += (context_matches.to_f / context_patterns.count) * 0.3
|
350
|
+
end
|
351
|
+
|
352
|
+
max_confidence = [max_confidence, confidence].max
|
353
|
+
end
|
354
|
+
|
355
|
+
max_confidence
|
356
|
+
end
|
357
|
+
|
358
|
+
def generate_recovery_suggestions(error_info, matching_patterns)
|
359
|
+
suggestions = []
|
360
|
+
|
361
|
+
# Add suggestions from matching patterns
|
362
|
+
matching_patterns.each do |pattern|
|
363
|
+
successful_recoveries = pattern['successful_recoveries'] || pattern[:successful_recoveries] || []
|
364
|
+
successful_recoveries.each do |recovery|
|
365
|
+
steps = recovery['steps'] || recovery[:steps] || []
|
366
|
+
suggestions << {
|
367
|
+
description: "Apply previously successful recovery: #{steps.join(' → ')}",
|
368
|
+
auto_executable: false,
|
369
|
+
action: 'manual_recovery',
|
370
|
+
steps: steps,
|
371
|
+
confidence: pattern[:confidence],
|
372
|
+
source: 'learned_pattern'
|
373
|
+
}
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Add generic recovery strategies
|
378
|
+
generic_strategies = get_generic_strategies_for_error(error_info)
|
379
|
+
suggestions.concat(generic_strategies)
|
380
|
+
|
381
|
+
# Sort by confidence and auto-executability
|
382
|
+
suggestions.sort_by { |s| [-s[:confidence], s[:auto_executable] ? 1 : 0] }
|
383
|
+
end
|
384
|
+
|
385
|
+
def get_generic_strategies_for_error(error_info)
|
386
|
+
strategies = []
|
387
|
+
|
388
|
+
# Network-related errors
|
389
|
+
if network_error?(error_info)
|
390
|
+
@recovery_strategies['network_errors'].each do |strategy|
|
391
|
+
strategies << strategy.merge(confidence: 0.7, source: 'generic')
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# File system errors
|
396
|
+
if file_system_error?(error_info)
|
397
|
+
if error_info[:message].include?('No such file')
|
398
|
+
@recovery_strategies['file_not_found'].each do |strategy|
|
399
|
+
strategies << strategy.merge(confidence: 0.8, source: 'generic')
|
400
|
+
end
|
401
|
+
elsif error_info[:message].include?('Permission denied')
|
402
|
+
@recovery_strategies['permission_denied'].each do |strategy|
|
403
|
+
strategies << strategy.merge(confidence: 0.8, source: 'generic')
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Dependency errors
|
409
|
+
if dependency_error?(error_info)
|
410
|
+
@recovery_strategies['dependency_missing'].each do |strategy|
|
411
|
+
strategies << strategy.merge(confidence: 0.7, source: 'generic')
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Timeout errors
|
416
|
+
if timeout_error?(error_info)
|
417
|
+
@recovery_strategies['timeout_error'].each do |strategy|
|
418
|
+
strategies << strategy.merge(confidence: 0.6, source: 'generic')
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Memory errors
|
423
|
+
if memory_error?(error_info)
|
424
|
+
@recovery_strategies['memory_error'].each do |strategy|
|
425
|
+
strategies << strategy.merge(confidence: 0.6, source: 'generic')
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
strategies
|
430
|
+
end
|
431
|
+
|
432
|
+
def auto_recoverable?(error_info, matching_patterns)
|
433
|
+
# Don't auto-recover critical system errors
|
434
|
+
return false if critical_error?(error_info)
|
435
|
+
|
436
|
+
# Check if any generic strategies are auto-executable
|
437
|
+
get_generic_strategies_for_error(error_info).any? { |s| s['auto_executable'] }
|
438
|
+
end
|
439
|
+
|
440
|
+
def execute_recovery_action(suggestion, agent_context)
|
441
|
+
action = suggestion['action'] || suggestion[:action]
|
442
|
+
|
443
|
+
case action
|
444
|
+
when 'retry_with_backoff'
|
445
|
+
retry_with_backoff(suggestion, agent_context)
|
446
|
+
when 'create_default_file'
|
447
|
+
create_default_file(suggestion, agent_context)
|
448
|
+
when 'find_similar_files'
|
449
|
+
find_similar_files(suggestion, agent_context)
|
450
|
+
when 'install_dependencies'
|
451
|
+
install_dependencies(suggestion, agent_context)
|
452
|
+
when 'retry_with_longer_timeout'
|
453
|
+
retry_with_longer_timeout(suggestion, agent_context)
|
454
|
+
when 'retry_with_reduced_memory'
|
455
|
+
retry_with_reduced_memory(suggestion, agent_context)
|
456
|
+
else
|
457
|
+
{ success: false, error: "Unknown recovery action: #{action}" }
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
def retry_with_backoff(suggestion, agent_context)
|
462
|
+
max_attempts = suggestion['max_attempts'] || 3
|
463
|
+
base_delay = suggestion['base_delay'] || 1
|
464
|
+
|
465
|
+
attempt = 1
|
466
|
+
|
467
|
+
while attempt <= max_attempts
|
468
|
+
begin
|
469
|
+
# Re-execute the original operation
|
470
|
+
if agent_context[:retry_block]
|
471
|
+
result = agent_context[:retry_block].call
|
472
|
+
return { success: true, result: result, attempts: attempt }
|
473
|
+
else
|
474
|
+
return { success: false, error: 'No retry block provided' }
|
475
|
+
end
|
476
|
+
rescue StandardError => e
|
477
|
+
if attempt == max_attempts
|
478
|
+
return { success: false, error: e.message, attempts: attempt }
|
479
|
+
end
|
480
|
+
|
481
|
+
delay = base_delay * (2 ** (attempt - 1))
|
482
|
+
sleep(delay)
|
483
|
+
attempt += 1
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def create_default_file(suggestion, agent_context)
|
489
|
+
file_path = agent_context[:file_path]
|
490
|
+
return { success: false, error: 'No file path provided' } unless file_path
|
491
|
+
|
492
|
+
begin
|
493
|
+
# Create directory if it doesn't exist
|
494
|
+
FileUtils.mkdir_p(File.dirname(file_path))
|
495
|
+
|
496
|
+
# Create file with default content based on extension
|
497
|
+
default_content = generate_default_file_content(file_path)
|
498
|
+
File.write(file_path, default_content)
|
499
|
+
|
500
|
+
{ success: true, file_created: file_path, content: default_content }
|
501
|
+
rescue StandardError => e
|
502
|
+
{ success: false, error: e.message }
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def find_similar_files(suggestion, agent_context)
|
507
|
+
file_path = agent_context[:file_path]
|
508
|
+
return { success: false, error: 'No file path provided' } unless file_path
|
509
|
+
|
510
|
+
begin
|
511
|
+
filename = File.basename(file_path)
|
512
|
+
directory = File.dirname(file_path)
|
513
|
+
extension = File.extname(file_path)
|
514
|
+
basename_without_ext = File.basename(filename, extension)
|
515
|
+
|
516
|
+
# Search for similar files
|
517
|
+
similar_files = Dir.glob("#{directory}/**/*#{extension}").select do |f|
|
518
|
+
existing_basename = File.basename(f, extension)
|
519
|
+
# Check if either file contains parts of the other's name
|
520
|
+
basename_without_ext.include?(existing_basename) ||
|
521
|
+
existing_basename.include?(basename_without_ext) ||
|
522
|
+
# Also check for common prefixes (e.g., "test" in both "test_new" and "test_file")
|
523
|
+
common_prefix_length(basename_without_ext, existing_basename) >= 3
|
524
|
+
end
|
525
|
+
|
526
|
+
{ success: true, similar_files: similar_files }
|
527
|
+
rescue StandardError => e
|
528
|
+
{ success: false, error: e.message }
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def install_dependencies(suggestion, agent_context)
|
533
|
+
begin
|
534
|
+
if File.exist?('package.json')
|
535
|
+
system('npm install')
|
536
|
+
{ success: true, package_manager: 'npm' }
|
537
|
+
elsif File.exist?('Gemfile')
|
538
|
+
system('bundle install')
|
539
|
+
{ success: true, package_manager: 'bundler' }
|
540
|
+
elsif File.exist?('requirements.txt')
|
541
|
+
system('pip install -r requirements.txt')
|
542
|
+
{ success: true, package_manager: 'pip' }
|
543
|
+
else
|
544
|
+
{ success: false, error: 'No recognized dependency file found' }
|
545
|
+
end
|
546
|
+
rescue StandardError => e
|
547
|
+
{ success: false, error: e.message }
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
def retry_with_longer_timeout(suggestion, agent_context)
|
552
|
+
multiplier = suggestion['timeout_multiplier'] || 2.0
|
553
|
+
original_timeout = agent_context[:timeout] || 30
|
554
|
+
new_timeout = (original_timeout * multiplier).to_i
|
555
|
+
|
556
|
+
begin
|
557
|
+
if agent_context[:retry_block]
|
558
|
+
result = Timeout.timeout(new_timeout) do
|
559
|
+
agent_context[:retry_block].call
|
560
|
+
end
|
561
|
+
{ success: true, result: result, new_timeout: new_timeout }
|
562
|
+
else
|
563
|
+
{ success: false, error: 'No retry block provided' }
|
564
|
+
end
|
565
|
+
rescue StandardError => e
|
566
|
+
{ success: false, error: e.message }
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
def retry_with_reduced_memory(suggestion, agent_context)
|
571
|
+
begin
|
572
|
+
# Force garbage collection
|
573
|
+
GC.start
|
574
|
+
|
575
|
+
# Re-execute with reduced memory profile
|
576
|
+
if agent_context[:retry_block]
|
577
|
+
result = agent_context[:retry_block].call
|
578
|
+
{ success: true, result: result }
|
579
|
+
else
|
580
|
+
{ success: false, error: 'No retry block provided' }
|
581
|
+
end
|
582
|
+
rescue StandardError => e
|
583
|
+
{ success: false, error: e.message }
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
def generate_default_file_content(file_path)
|
588
|
+
extension = File.extname(file_path).downcase
|
589
|
+
|
590
|
+
case extension
|
591
|
+
when '.rb'
|
592
|
+
"# frozen_string_literal: true\n\n# Default Ruby file\n"
|
593
|
+
when '.js'
|
594
|
+
"// Default JavaScript file\n"
|
595
|
+
when '.ts'
|
596
|
+
"// Default TypeScript file\nexport {};\n"
|
597
|
+
when '.py'
|
598
|
+
"#!/usr/bin/env python3\n# Default Python file\n"
|
599
|
+
when '.yml', '.yaml'
|
600
|
+
"# Default YAML configuration\n"
|
601
|
+
when '.json'
|
602
|
+
"{}\n"
|
603
|
+
when '.md'
|
604
|
+
"# #{File.basename(file_path, extension).gsub(/[_-]/, ' ').split.map(&:capitalize).join(' ')}\n\nDefault content.\n"
|
605
|
+
else
|
606
|
+
"# Default content for #{file_path}\n"
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
def network_error?(error_info)
|
611
|
+
network_patterns = [
|
612
|
+
'connection', 'network', 'timeout', 'unreachable', 'dns',
|
613
|
+
'socket', 'ssl', 'certificate', 'refused', 'reset'
|
614
|
+
]
|
615
|
+
|
616
|
+
message_lower = error_info[:message].downcase
|
617
|
+
network_patterns.any? { |pattern| message_lower.include?(pattern) }
|
618
|
+
end
|
619
|
+
|
620
|
+
def file_system_error?(error_info)
|
621
|
+
fs_patterns = [
|
622
|
+
'no such file', 'permission denied', 'file not found',
|
623
|
+
'directory not found', 'access denied', 'file exists'
|
624
|
+
]
|
625
|
+
|
626
|
+
message_lower = error_info[:message].downcase
|
627
|
+
fs_patterns.any? { |pattern| message_lower.include?(pattern) }
|
628
|
+
end
|
629
|
+
|
630
|
+
def dependency_error?(error_info)
|
631
|
+
dep_patterns = [
|
632
|
+
'cannot load', 'not found', 'missing', 'uninitialized constant',
|
633
|
+
'module not found', 'import error', 'no such gem'
|
634
|
+
]
|
635
|
+
|
636
|
+
message_lower = error_info[:message].downcase
|
637
|
+
dep_patterns.any? { |pattern| message_lower.include?(pattern) }
|
638
|
+
end
|
639
|
+
|
640
|
+
def timeout_error?(error_info)
|
641
|
+
error_info[:type].include?('Timeout') ||
|
642
|
+
error_info[:message].downcase.include?('timeout')
|
643
|
+
end
|
644
|
+
|
645
|
+
def memory_error?(error_info)
|
646
|
+
memory_patterns = ['memory', 'out of memory', 'cannot allocate']
|
647
|
+
|
648
|
+
message_lower = error_info[:message].downcase
|
649
|
+
memory_patterns.any? { |pattern| message_lower.include?(pattern) }
|
650
|
+
end
|
651
|
+
|
652
|
+
def critical_error?(error_info)
|
653
|
+
critical_patterns = [
|
654
|
+
'system', 'kernel', 'segmentation fault', 'access violation',
|
655
|
+
'stack overflow', 'fatal'
|
656
|
+
]
|
657
|
+
|
658
|
+
message_lower = error_info[:message].downcase
|
659
|
+
critical_patterns.any? { |pattern| message_lower.include?(pattern) }
|
660
|
+
end
|
661
|
+
|
662
|
+
def generate_generic_explanation(error_info)
|
663
|
+
{
|
664
|
+
explanation: "An error of type #{error_info[:type]} occurred",
|
665
|
+
likely_cause: "The specific cause is unclear from the available information",
|
666
|
+
prevention_tips: [
|
667
|
+
"Check the error message for specific details",
|
668
|
+
"Verify that all dependencies are properly installed",
|
669
|
+
"Ensure file permissions are correct",
|
670
|
+
"Check for typos in file paths or commands"
|
671
|
+
]
|
672
|
+
}
|
673
|
+
end
|
674
|
+
|
675
|
+
def log_error_occurrence(error_info)
|
676
|
+
@recovery_history << {
|
677
|
+
error: error_info,
|
678
|
+
timestamp: Time.now.iso8601,
|
679
|
+
recovery_attempted: false,
|
680
|
+
recovery_successful: false
|
681
|
+
}
|
682
|
+
end
|
683
|
+
|
684
|
+
def log_successful_recovery(error_info, recovery_strategy)
|
685
|
+
# Update the most recent entry
|
686
|
+
if @recovery_history.last && @recovery_history.last[:error] == error_info
|
687
|
+
@recovery_history.last[:recovery_attempted] = true
|
688
|
+
@recovery_history.last[:recovery_successful] = true
|
689
|
+
@recovery_history.last[:recovery_strategy] = recovery_strategy
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
def log_recovery_attempts(error_info, attempts)
|
694
|
+
# Update the most recent entry
|
695
|
+
if @recovery_history.last && @recovery_history.last[:error] == error_info
|
696
|
+
@recovery_history.last[:recovery_attempted] = true
|
697
|
+
@recovery_history.last[:recovery_successful] = attempts.any? { |a| a[:success] }
|
698
|
+
@recovery_history.last[:recovery_attempts] = attempts
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
def most_common_error_types
|
703
|
+
error_counts = @recovery_history.group_by { |h| h[:error][:type] }
|
704
|
+
.transform_values(&:count)
|
705
|
+
|
706
|
+
error_counts.sort_by { |_, count| -count }.first(5).to_h
|
707
|
+
end
|
708
|
+
|
709
|
+
def extract_error_signature(error_info)
|
710
|
+
{
|
711
|
+
'error_type' => error_info[:type],
|
712
|
+
'message_pattern' => extract_message_pattern(error_info[:message]),
|
713
|
+
'context_patterns' => extract_context_patterns(error_info[:context])
|
714
|
+
}
|
715
|
+
end
|
716
|
+
|
717
|
+
def extract_message_pattern(message)
|
718
|
+
# Extract key words and remove specific paths/values
|
719
|
+
pattern = message.gsub(/\/[\/\w.-]+/, '<PATH>')
|
720
|
+
.gsub(/\d+/, '<NUMBER>')
|
721
|
+
.gsub(/[a-f0-9]{8,}/, '<HASH>')
|
722
|
+
.strip
|
723
|
+
|
724
|
+
# Take first meaningful part
|
725
|
+
pattern.split(/[:.!]/).first&.strip || pattern
|
726
|
+
end
|
727
|
+
|
728
|
+
def extract_context_patterns(context)
|
729
|
+
return [] unless context.is_a?(Hash)
|
730
|
+
|
731
|
+
patterns = []
|
732
|
+
context.each do |key, value|
|
733
|
+
patterns << "#{key}:#{value.class.name}" if value
|
734
|
+
end
|
735
|
+
patterns
|
736
|
+
end
|
737
|
+
|
738
|
+
def generate_pattern_key(error_info)
|
739
|
+
# Generate a consistent key for grouping similar errors
|
740
|
+
key_parts = [
|
741
|
+
error_info[:type],
|
742
|
+
extract_message_pattern(error_info[:message])
|
743
|
+
]
|
744
|
+
|
745
|
+
Digest::SHA256.hexdigest(key_parts.join('|'))[0, 16]
|
746
|
+
end
|
747
|
+
|
748
|
+
def common_prefix_length(str1, str2)
|
749
|
+
return 0 if str1.nil? || str2.nil?
|
750
|
+
|
751
|
+
min_length = [str1.length, str2.length].min
|
752
|
+
(0...min_length).each do |i|
|
753
|
+
return i if str1[i].downcase != str2[i].downcase
|
754
|
+
end
|
755
|
+
min_length
|
756
|
+
end
|
757
|
+
|
758
|
+
# Class methods for singleton access
|
759
|
+
class << self
|
760
|
+
def analyze_error(*args)
|
761
|
+
instance.analyze_error(*args)
|
762
|
+
end
|
763
|
+
|
764
|
+
def attempt_recovery(*args)
|
765
|
+
instance.attempt_recovery(*args)
|
766
|
+
end
|
767
|
+
|
768
|
+
def explain_error(*args)
|
769
|
+
instance.explain_error(*args)
|
770
|
+
end
|
771
|
+
|
772
|
+
def learn_from_manual_recovery(*args)
|
773
|
+
instance.learn_from_manual_recovery(*args)
|
774
|
+
end
|
775
|
+
|
776
|
+
def recovery_statistics
|
777
|
+
instance.recovery_statistics
|
778
|
+
end
|
779
|
+
|
780
|
+
def cleanup_old_data(*args)
|
781
|
+
instance.cleanup_old_data(*args)
|
782
|
+
end
|
783
|
+
end
|
784
|
+
end
|
785
|
+
end
|