hanuscode 1.0.0__py3-none-any.whl

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.
Files changed (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. hanuscode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,605 @@
1
+ # hanus/error_recovery/auto_fix.py
2
+ """
3
+ Sistema de auto-corrección de errores.
4
+
5
+ Funcionalidades:
6
+ - Detectar errores en salida de comandos
7
+ - Proponer fixes automáticamente
8
+ - Learning de errores comunes por proyecto
9
+ - Base de conocimiento de soluciones
10
+ """
11
+ from __future__ import annotations
12
+ import re
13
+ import json
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Dict, List, Optional, Any, Callable, Tuple
18
+ from datetime import datetime
19
+ from enum import Enum
20
+
21
+
22
+ class ErrorSeverity(Enum):
23
+ """Severidad del error."""
24
+ LOW = "low" # Warning, puede ignorarse
25
+ MEDIUM = "medium" # Error pero hay workaround
26
+ HIGH = "high" # Error crítico, requiere fix
27
+ CRITICAL = "critical" # Error que impide continuar
28
+
29
+
30
+ class FixType(Enum):
31
+ """Tipo de fix sugerido."""
32
+ COMMAND_FIX = "command_fix" # Corregir comando
33
+ FILE_EDIT = "file_edit" # Editar archivo
34
+ PACKAGE_INSTALL = "package" # Instalar paquete
35
+ CONFIG_CHANGE = "config" # Cambiar configuración
36
+ PERMISSION_FIX = "permission" # Arreglar permisos
37
+ ENVIRONMENT = "environment" # Problema de entorno
38
+ SYNTAX_FIX = "syntax" # Error de sintaxis
39
+ DEPENDENCY = "dependency" # Problema de dependencia
40
+
41
+
42
+ @dataclass
43
+ class ErrorPattern:
44
+ """Patrón de error reconocido."""
45
+ id: str
46
+ name: str
47
+ pattern: str # Regex pattern
48
+ severity: ErrorSeverity
49
+ fix_type: FixType
50
+ description: str
51
+ fix_template: str # Template para el fix
52
+ fix_command: Optional[str] = None # Comando para arreglar
53
+ auto_fixable: bool = True
54
+ tags: List[str] = field(default_factory=list)
55
+
56
+
57
+ @dataclass
58
+ class FixSuggestion:
59
+ """Sugerencia de fix para un error."""
60
+ error_id: str
61
+ error_message: str
62
+ error_type: str
63
+ severity: ErrorSeverity
64
+ fix_type: FixType
65
+ description: str
66
+ fix_command: Optional[str] = None
67
+ fix_file: Optional[str] = None
68
+ fix_content: Optional[str] = None
69
+ confidence: float = 0.0 # 0-1, confianza en el fix
70
+ auto_apply: bool = False # Si se puede aplicar automáticamente
71
+ requires_confirmation: bool = True
72
+ metadata: Dict[str, Any] = field(default_factory=dict)
73
+
74
+ def to_dict(self) -> Dict:
75
+ return {
76
+ "error_id": self.error_id,
77
+ "error_message": self.error_message,
78
+ "error_type": self.error_type,
79
+ "severity": self.severity.value,
80
+ "fix_type": self.fix_type.value,
81
+ "description": self.description,
82
+ "fix_command": self.fix_command,
83
+ "fix_file": self.fix_file,
84
+ "fix_content": self.fix_content,
85
+ "confidence": self.confidence,
86
+ "auto_apply": self.auto_apply,
87
+ "requires_confirmation": self.requires_confirmation,
88
+ }
89
+
90
+
91
+ class ErrorRecovery:
92
+ """
93
+ Sistema de detección y corrección automática de errores.
94
+
95
+ Características:
96
+ - Patrones de error predefinidos
97
+ - Learning de errores del proyecto
98
+ - Sugerencias de fix con confianza
99
+ - Auto-aplicación de fixes seguros
100
+ """
101
+
102
+ # Patrones de error predefinidos
103
+ BUILTIN_PATTERNS = [
104
+ # Python errors
105
+ ErrorPattern(
106
+ id="python_module_not_found",
107
+ name="Module Not Found",
108
+ pattern=r"ModuleNotFoundError: No module named '([^']+)'",
109
+ severity=ErrorSeverity.HIGH,
110
+ fix_type=FixType.PACKAGE_INSTALL,
111
+ description="Missing Python module",
112
+ fix_template="pip install {module}",
113
+ fix_command="pip install {module}",
114
+ auto_fixable=True,
115
+ tags=["python", "import"]
116
+ ),
117
+ ErrorPattern(
118
+ id="python_syntax_error",
119
+ name="Syntax Error",
120
+ pattern=r"SyntaxError: (.+)",
121
+ severity=ErrorSeverity.HIGH,
122
+ fix_type=FixType.SYNTAX_FIX,
123
+ description="Python syntax error",
124
+ fix_template="Check syntax in file",
125
+ auto_fixable=False,
126
+ tags=["python", "syntax"]
127
+ ),
128
+ ErrorPattern(
129
+ id="python_indentation_error",
130
+ name="Indentation Error",
131
+ pattern=r"IndentationError: (.+)",
132
+ severity=ErrorSeverity.HIGH,
133
+ fix_type=FixType.SYNTAX_FIX,
134
+ description="Python indentation error",
135
+ fix_template="Fix indentation",
136
+ auto_fixable=False,
137
+ tags=["python", "indentation"]
138
+ ),
139
+ ErrorPattern(
140
+ id="python_attribute_error",
141
+ name="Attribute Error",
142
+ pattern=r"AttributeError: '(\w+)' object has no attribute '(\w+)'",
143
+ severity=ErrorSeverity.MEDIUM,
144
+ fix_type=FixType.FILE_EDIT,
145
+ description="Object missing attribute",
146
+ fix_template="Check if attribute exists or object type is correct",
147
+ auto_fixable=False,
148
+ tags=["python", "attribute"]
149
+ ),
150
+ ErrorPattern(
151
+ id="python_type_error",
152
+ name="Type Error",
153
+ pattern=r"TypeError: (.+)",
154
+ severity=ErrorSeverity.MEDIUM,
155
+ fix_type=FixType.FILE_EDIT,
156
+ description="Type mismatch",
157
+ fix_template="Check types and conversions",
158
+ auto_fixable=False,
159
+ tags=["python", "type"]
160
+ ),
161
+ ErrorPattern(
162
+ id="python_index_error",
163
+ name="Index Error",
164
+ pattern=r"IndexError: (.+)",
165
+ severity=ErrorSeverity.MEDIUM,
166
+ fix_type=FixType.FILE_EDIT,
167
+ description="List index out of range",
168
+ fix_template="Check list length before indexing",
169
+ auto_fixable=False,
170
+ tags=["python", "index"]
171
+ ),
172
+ ErrorPattern(
173
+ id="python_key_error",
174
+ name="Key Error",
175
+ pattern=r"KeyError: '([^']+)'",
176
+ severity=ErrorSeverity.MEDIUM,
177
+ fix_type=FixType.FILE_EDIT,
178
+ description="Dictionary key not found",
179
+ fix_template="Check if key exists: if '{key}' in dict:",
180
+ auto_fixable=False,
181
+ tags=["python", "dict"]
182
+ ),
183
+ ErrorPattern(
184
+ id="python_file_not_found",
185
+ name="File Not Found",
186
+ pattern=r"FileNotFoundError: \[Errno 2\] No such file or directory: '([^']+)'",
187
+ severity=ErrorSeverity.HIGH,
188
+ fix_type=FixType.FILE_EDIT,
189
+ description="File does not exist",
190
+ fix_template="Create file or check path",
191
+ auto_fixable=False,
192
+ tags=["python", "file"]
193
+ ),
194
+
195
+ # Node.js errors
196
+ ErrorPattern(
197
+ id="node_module_not_found",
198
+ name="Node Module Not Found",
199
+ pattern=r"Error: Cannot find module '([^']+)'",
200
+ severity=ErrorSeverity.HIGH,
201
+ fix_type=FixType.PACKAGE_INSTALL,
202
+ description="Missing Node.js module",
203
+ fix_template="npm install {module}",
204
+ fix_command="npm install {module}",
205
+ auto_fixable=True,
206
+ tags=["nodejs", "import"]
207
+ ),
208
+ ErrorPattern(
209
+ id="node_syntax_error",
210
+ name="JavaScript Syntax Error",
211
+ pattern=r"SyntaxError: (.+)",
212
+ severity=ErrorSeverity.HIGH,
213
+ fix_type=FixType.SYNTAX_FIX,
214
+ description="JavaScript syntax error",
215
+ fix_template="Check syntax",
216
+ auto_fixable=False,
217
+ tags=["nodejs", "syntax"]
218
+ ),
219
+
220
+ # Git errors
221
+ ErrorPattern(
222
+ id="git_not_a_repository",
223
+ name="Not a Git Repository",
224
+ pattern=r"fatal: not a git repository",
225
+ severity=ErrorSeverity.HIGH,
226
+ fix_type=FixType.COMMAND_FIX,
227
+ description="Not in a git repository",
228
+ fix_template="git init",
229
+ fix_command="git init",
230
+ auto_fixable=True,
231
+ tags=["git"]
232
+ ),
233
+ ErrorPattern(
234
+ id="git_merge_conflict",
235
+ name="Git Merge Conflict",
236
+ pattern=r"CONFLICT \\(content\\): Merge conflict in (.+)",
237
+ severity=ErrorSeverity.HIGH,
238
+ fix_type=FixType.FILE_EDIT,
239
+ description="Merge conflict in file",
240
+ fix_template="Resolve conflicts in {file}",
241
+ auto_fixable=False,
242
+ tags=["git", "merge"]
243
+ ),
244
+ ErrorPattern(
245
+ id="git_uncommitted_changes",
246
+ name="Uncommitted Changes",
247
+ pattern=r"error: Your local changes to the following files would be overwritten",
248
+ severity=ErrorSeverity.MEDIUM,
249
+ fix_type=FixType.COMMAND_FIX,
250
+ description="Uncommitted local changes",
251
+ fix_template="git stash or commit changes",
252
+ auto_fixable=False,
253
+ tags=["git"]
254
+ ),
255
+
256
+ # Permission errors
257
+ ErrorPattern(
258
+ id="permission_denied",
259
+ name="Permission Denied",
260
+ pattern=r"Permission denied: '([^']+)'|EACCES: permission denied",
261
+ severity=ErrorSeverity.HIGH,
262
+ fix_type=FixType.PERMISSION_FIX,
263
+ description="Permission denied",
264
+ fix_template="chmod +x {file} or run with sudo",
265
+ fix_command="chmod +x {file}",
266
+ auto_fixable=True,
267
+ tags=["permission"]
268
+ ),
269
+
270
+ # Command not found
271
+ ErrorPattern(
272
+ id="command_not_found",
273
+ name="Command Not Found",
274
+ pattern=r"command not found: (\w+)|'(\w+)' is not recognized",
275
+ severity=ErrorSeverity.HIGH,
276
+ fix_type=FixType.ENVIRONMENT,
277
+ description="Command not installed or not in PATH",
278
+ fix_template="Install {command} or check PATH",
279
+ auto_fixable=False,
280
+ tags=["environment", "command"]
281
+ ),
282
+
283
+ # Network errors
284
+ ErrorPattern(
285
+ id="connection_refused",
286
+ name="Connection Refused",
287
+ pattern=r"Connection refused|ECONNREFUSED",
288
+ severity=ErrorSeverity.MEDIUM,
289
+ fix_type=FixType.ENVIRONMENT,
290
+ description="Cannot connect to server",
291
+ fix_template="Check if service is running",
292
+ auto_fixable=False,
293
+ tags=["network"]
294
+ ),
295
+ ErrorPattern(
296
+ id="dns_resolution",
297
+ name="DNS Resolution Failed",
298
+ pattern=r"getaddrinfo ENOTFOUND|Temporary failure in name resolution",
299
+ severity=ErrorSeverity.MEDIUM,
300
+ fix_type=FixType.ENVIRONMENT,
301
+ description="DNS resolution failed",
302
+ fix_template="Check internet connection or DNS settings",
303
+ auto_fixable=False,
304
+ tags=["network", "dns"]
305
+ ),
306
+
307
+ # Build errors
308
+ ErrorPattern(
309
+ id="missing_make_target",
310
+ name="Missing Make Target",
311
+ pattern=r"make: \*\*\* No rule to make target '([^']+)'.",
312
+ severity=ErrorSeverity.MEDIUM,
313
+ fix_type=FixType.FILE_EDIT,
314
+ description="Makefile missing target",
315
+ fix_template="Add target to Makefile or check spelling",
316
+ auto_fixable=False,
317
+ tags=["build", "make"]
318
+ ),
319
+ ]
320
+
321
+ def __init__(self, project_dir: Optional[Path] = None):
322
+ self.project_dir = project_dir or Path.cwd()
323
+
324
+ # Patrones de error (builtins + aprendidos)
325
+ self._patterns: Dict[str, ErrorPattern] = {p.id: p for p in self.BUILTIN_PATTERNS}
326
+ self._learned_patterns: Dict[str, ErrorPattern] = {}
327
+
328
+ # Historial de errores del proyecto
329
+ self._error_history: List[Dict] = []
330
+
331
+ # Base de conocimiento de fixes aplicados
332
+ self._fixes_applied: Dict[str, int] = {} # error_id -> count
333
+
334
+ # Archivos de persistencia
335
+ self._learned_file = Path.home() / ".hanus" / "error_recovery" / "learned_patterns.json"
336
+ self._history_file = Path.home() / ".hanus" / "error_recovery" / "history.json"
337
+ self._load_learned()
338
+
339
+ def analyze_error(self, output: str) -> Optional[FixSuggestion]:
340
+ """
341
+ Analiza la salida de un comando para detectar errores y sugerir fixes.
342
+
343
+ Args:
344
+ output: Salida del comando (stdout + stderr)
345
+
346
+ Returns:
347
+ FixSuggestion si se encontró un error conocido, None si no
348
+ """
349
+ # Buscar en patrones builtin y aprendidos
350
+ all_patterns = {**self._patterns, **self._learned_patterns}
351
+
352
+ for pattern in all_patterns.values():
353
+ match = re.search(pattern.pattern, output, re.IGNORECASE | re.MULTILINE)
354
+ if match:
355
+ suggestion = self._create_suggestion(pattern, match, output)
356
+ self._record_error(suggestion)
357
+ return suggestion
358
+
359
+ # Si no encontramos patrón, intentar análisis genérico
360
+ generic = self._generic_error_analysis(output)
361
+ if generic:
362
+ self._record_error(generic)
363
+ return generic
364
+
365
+ return None
366
+
367
+ def apply_fix(self, suggestion: FixSuggestion) -> Tuple[bool, str]:
368
+ """
369
+ Aplica un fix sugerido.
370
+
371
+ Args:
372
+ suggestion: La sugerencia de fix
373
+
374
+ Returns:
375
+ (success, message)
376
+ """
377
+ if not suggestion.auto_apply and suggestion.requires_confirmation:
378
+ return False, "This fix requires confirmation before applying"
379
+
380
+ if suggestion.fix_type == FixType.PACKAGE_INSTALL and suggestion.fix_command:
381
+ # Registrar fix aplicado
382
+ self._fixes_applied[suggestion.error_id] = self._fixes_applied.get(suggestion.error_id, 0) + 1
383
+ return True, f"Run: {suggestion.fix_command}"
384
+
385
+ if suggestion.fix_type == FixType.COMMAND_FIX and suggestion.fix_command:
386
+ self._fixes_applied[suggestion.error_id] = self._fixes_applied.get(suggestion.error_id, 0) + 1
387
+ return True, f"Run: {suggestion.fix_command}"
388
+
389
+ if suggestion.fix_type == FixType.FILE_EDIT and suggestion.fix_file:
390
+ self._fixes_applied[suggestion.error_id] = self._fixes_applied.get(suggestion.error_id, 0) + 1
391
+ return True, f"Edit {suggestion.fix_file}: {suggestion.description}"
392
+
393
+ return False, "No automatic fix available"
394
+
395
+ def learn_from_fix(self, error_message: str, fix_description: str, fix_command: str = None):
396
+ """
397
+ Aprende de un fix aplicado para futuras sugerencias.
398
+
399
+ Args:
400
+ error_message: El mensaje de error original
401
+ fix_description: Descripción del fix aplicado
402
+ fix_command: Comando ejecutado para arreglar (si aplica)
403
+ """
404
+ # Crear patrón simple
405
+ pattern_id = f"learned_{int(time.time())}"
406
+
407
+ # Extraer patrón del error
408
+ # Simplificar el mensaje para crear un patrón regex básico
409
+ simplified = re.escape(error_message[:50])
410
+ simplified = re.sub(r'\\s+', r'\\s+', simplified)
411
+
412
+ pattern = ErrorPattern(
413
+ id=pattern_id,
414
+ name=f"Learned: {fix_description[:30]}",
415
+ pattern=simplified,
416
+ severity=ErrorSeverity.MEDIUM,
417
+ fix_type=FixType.COMMAND_FIX if fix_command else FixType.FILE_EDIT,
418
+ description=fix_description,
419
+ fix_template=fix_description,
420
+ fix_command=fix_command,
421
+ auto_fixable=bool(fix_command),
422
+ tags=["learned"]
423
+ )
424
+
425
+ self._learned_patterns[pattern_id] = pattern
426
+ self._save_learned()
427
+
428
+ def get_error_stats(self) -> Dict[str, Any]:
429
+ """Obtiene estadísticas de errores."""
430
+ return {
431
+ "total_errors": len(self._error_history),
432
+ "patterns_known": len(self._patterns) + len(self._learned_patterns),
433
+ "learned_patterns": len(self._learned_patterns),
434
+ "fixes_applied": sum(self._fixes_applied.values()),
435
+ "top_errors": sorted(
436
+ self._fixes_applied.items(),
437
+ key=lambda x: -x[1]
438
+ )[:5],
439
+ }
440
+
441
+ def get_error_history(self, limit: int = 50) -> List[Dict]:
442
+ """Obtiene el historial de errores."""
443
+ return self._error_history[-limit:]
444
+
445
+ # ══════════════════════════════════════════════════════════════════════════
446
+ # MÉTODOS PRIVADOS
447
+ # ══════════════════════════════════════════════════════════════════════════
448
+
449
+ def _create_suggestion(
450
+ self,
451
+ pattern: ErrorPattern,
452
+ match: re.Match,
453
+ output: str
454
+ ) -> FixSuggestion:
455
+ """Crea una sugerencia de fix a partir de un patrón encontrado."""
456
+
457
+ # Extraer grupos del match para personalizar el fix
458
+ groups = match.groups() if match.groups() else []
459
+ group_dict = match.groupdict() if hasattr(match, 'groupdict') else {}
460
+
461
+ # Personalizar fix con grupos capturados
462
+ fix_command = pattern.fix_command
463
+ fix_template = pattern.fix_template
464
+
465
+ if groups:
466
+ # Primer grupo como variable principal
467
+ main_var = groups[0] if groups else ""
468
+ if fix_command:
469
+ fix_command = fix_command.format(module=main_var, file=main_var, command=main_var, key=main_var)
470
+ fix_template = fix_template.format(module=main_var, file=main_var, command=main_var, key=main_var)
471
+
472
+ # Calcular confianza basada en historial
473
+ confidence = 0.5
474
+ if pattern.id in self._fixes_applied:
475
+ # Más confianza si el fix se ha aplicado antes con éxito
476
+ confidence = min(0.95, 0.5 + self._fixes_applied[pattern.id] * 0.1)
477
+
478
+ return FixSuggestion(
479
+ error_id=pattern.id,
480
+ error_message=match.group(0),
481
+ error_type=pattern.name,
482
+ severity=pattern.severity,
483
+ fix_type=pattern.fix_type,
484
+ description=fix_template,
485
+ fix_command=fix_command,
486
+ confidence=confidence,
487
+ auto_apply=pattern.auto_fixable and confidence > 0.7,
488
+ requires_confirmation=not pattern.auto_fixable,
489
+ metadata={"pattern": pattern.id, "groups": groups}
490
+ )
491
+
492
+ def _generic_error_analysis(self, output: str) -> Optional[FixSuggestion]:
493
+ """Análisis genérico cuando no hay patrón conocido."""
494
+ # Buscar palabras clave de error
495
+ error_keywords = ["error", "failed", "exception", "fatal", "critical"]
496
+
497
+ for keyword in error_keywords:
498
+ if keyword in output.lower():
499
+ # Encontrar la línea con el error
500
+ lines = output.split('\n')
501
+ error_line = None
502
+ for line in lines:
503
+ if keyword in line.lower():
504
+ error_line = line.strip()
505
+ break
506
+
507
+ if error_line:
508
+ return FixSuggestion(
509
+ error_id="generic",
510
+ error_message=error_line[:200],
511
+ error_type="Generic Error",
512
+ severity=ErrorSeverity.MEDIUM,
513
+ fix_type=FixType.FILE_EDIT,
514
+ description="Review error and fix manually",
515
+ confidence=0.3,
516
+ auto_apply=False,
517
+ requires_confirmation=True,
518
+ )
519
+
520
+ return None
521
+
522
+ def _record_error(self, suggestion: FixSuggestion) -> None:
523
+ """Registra un error en el historial."""
524
+ self._error_history.append({
525
+ "timestamp": time.time(),
526
+ "error_id": suggestion.error_id,
527
+ "error_type": suggestion.error_type,
528
+ "error_message": suggestion.error_message[:200],
529
+ "severity": suggestion.severity.value,
530
+ })
531
+
532
+ # Limitar historial
533
+ if len(self._error_history) > 1000:
534
+ self._error_history = self._error_history[-500:]
535
+
536
+ def _load_learned(self) -> None:
537
+ """Carga patrones aprendidos desde disco."""
538
+ if self._learned_file.exists():
539
+ try:
540
+ data = json.loads(self._learned_file.read_text(encoding="utf-8"))
541
+ for pattern_data in data.get("patterns", []):
542
+ pattern = ErrorPattern(
543
+ id=pattern_data["id"],
544
+ name=pattern_data["name"],
545
+ pattern=pattern_data["pattern"],
546
+ severity=ErrorSeverity(pattern_data["severity"]),
547
+ fix_type=FixType(pattern_data["fix_type"]),
548
+ description=pattern_data["description"],
549
+ fix_template=pattern_data["fix_template"],
550
+ fix_command=pattern_data.get("fix_command"),
551
+ auto_fixable=pattern_data.get("auto_fixable", False),
552
+ tags=pattern_data.get("tags", []),
553
+ )
554
+ self._learned_patterns[pattern.id] = pattern
555
+
556
+ self._fixes_applied = data.get("fixes_applied", {})
557
+ except Exception:
558
+ pass
559
+
560
+ def _save_learned(self) -> None:
561
+ """Guarda patrones aprendidos a disco."""
562
+ try:
563
+ self._learned_file.parent.mkdir(parents=True, exist_ok=True)
564
+
565
+ data = {
566
+ "patterns": [
567
+ {
568
+ "id": p.id,
569
+ "name": p.name,
570
+ "pattern": p.pattern,
571
+ "severity": p.severity.value,
572
+ "fix_type": p.fix_type.value,
573
+ "description": p.description,
574
+ "fix_template": p.fix_template,
575
+ "fix_command": p.fix_command,
576
+ "auto_fixable": p.auto_fixable,
577
+ "tags": p.tags,
578
+ }
579
+ for p in self._learned_patterns.values()
580
+ ],
581
+ "fixes_applied": self._fixes_applied,
582
+ "updated": datetime.now().isoformat(),
583
+ }
584
+
585
+ self._learned_file.write_text(
586
+ json.dumps(data, indent=2, ensure_ascii=False),
587
+ encoding="utf-8"
588
+ )
589
+ except Exception:
590
+ pass
591
+
592
+
593
+ # ══════════════════════════════════════════════════════════════════════════════
594
+ # INSTANCIA GLOBAL
595
+ # ══════════════════════════════════════════════════════════════════════════════
596
+
597
+ _recovery_instance: Optional[ErrorRecovery] = None
598
+
599
+
600
+ def get_error_recovery() -> ErrorRecovery:
601
+ """Obtiene la instancia global del sistema de recuperación."""
602
+ global _recovery_instance
603
+ if _recovery_instance is None:
604
+ _recovery_instance = ErrorRecovery()
605
+ return _recovery_instance
@@ -0,0 +1,5 @@
1
+ # hanus/hooks/__init__.py
2
+ """Lifecycle hooks for HanusCode."""
3
+ from .manager import HookManager, hook, HookType
4
+
5
+ __all__ = ["HookManager", "hook", "HookType"]