snapcontext 2.0.0__tar.gz → 2.2.0__tar.gz

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 (39) hide show
  1. {snapcontext-2.0.0/snapcontext.egg-info → snapcontext-2.2.0}/PKG-INFO +21 -4
  2. {snapcontext-2.0.0 → snapcontext-2.2.0}/README.md +20 -3
  3. snapcontext-2.2.0/agentes.py +423 -0
  4. {snapcontext-2.0.0 → snapcontext-2.2.0}/orquestador.py +11 -33
  5. {snapcontext-2.0.0 → snapcontext-2.2.0}/pyproject.toml +89 -89
  6. {snapcontext-2.0.0 → snapcontext-2.2.0/snapcontext.egg-info}/PKG-INFO +21 -4
  7. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/SOURCES.txt +2 -0
  8. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.py +416 -38
  9. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_auto_017.py +1 -1
  10. snapcontext-2.2.0/tests/test_diff_editor_210.py +103 -0
  11. snapcontext-2.2.0/tests/test_editor_ast_220.py +181 -0
  12. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_editor_propio_200.py +1 -1
  13. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_exe_150.py +1 -1
  14. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_jetbrains_170.py +2 -2
  15. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_mcp_014.py +1 -1
  16. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_mcp_140.py +1 -1
  17. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_memoria_015.py +1 -1
  18. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_permisos_013.py +1 -1
  19. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_plan_012.py +1 -1
  20. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_v010.py +1 -1
  21. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_validacion_130.py +1 -1
  22. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_vscode_016.py +2 -2
  23. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_web_160.py +1 -1
  24. {snapcontext-2.0.0 → snapcontext-2.2.0}/web/app.py +392 -392
  25. snapcontext-2.0.0/agentes.py +0 -209
  26. {snapcontext-2.0.0 → snapcontext-2.2.0}/LICENSE +0 -0
  27. {snapcontext-2.0.0 → snapcontext-2.2.0}/setup.cfg +0 -0
  28. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
  29. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/dependency_links.txt +0 -0
  30. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/entry_points.txt +0 -0
  31. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/requires.txt +0 -0
  32. {snapcontext-2.0.0 → snapcontext-2.2.0}/snapcontext.egg-info/top_level.txt +0 -0
  33. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_agentes.py +0 -0
  34. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_deteccion.py +0 -0
  35. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_embeddings_110.py +0 -0
  36. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_orquestador.py +0 -0
  37. {snapcontext-2.0.0 → snapcontext-2.2.0}/tests/test_web_120.py +0 -0
  38. {snapcontext-2.0.0 → snapcontext-2.2.0}/web/__init__.py +0 -0
  39. {snapcontext-2.0.0 → snapcontext-2.2.0}/web/static/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapcontext
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
5
5
  Author-email: SnapContext Contributors <tu@email.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
 
46
46
  # SnapContext
47
47
 
48
- ![v2.0.0](https://img.shields.io/badge/version-2.0.0-blue.svg)
48
+ ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
49
49
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
50
50
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
51
51
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -104,14 +104,31 @@ snapcontext interactive # o snapcontext --web
104
104
  Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
105
105
  carpetas, extensiones y comandos de test por defecto.
106
106
 
107
- ## 🛠️ Editor Integrado Propio (v2.0.0)
107
+ ## 🛠️ Editor Integrado Propio (v2.2.0)
108
108
 
109
- SnapContext v2.0.0 incluye su propio editor integrado:
109
+ SnapContext incluye su propio editor integrado con soporte de parches unificados
110
+ y edición basada en AST (Árbol Sintáctico) para refactorizaciones precisas:
110
111
  - **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
112
+ - **`--modo-edicion {auto,parche,sobrescribir,ast}`**:
113
+ - `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`), pero
114
+ para refactorizaciones estructurales intenta primero la edición **AST** y cae a
115
+ sobrescritura si nada es aplicable.
116
+ - `ast`: Edita comprendiendo la estructura sintáctica del archivo (Python con
117
+ `ast`, otros lenguajes con `tree-sitter` si está instalado). Soporta renombrar
118
+ símbolos, insertar imports y recibir el código completo resultante.
119
+ - `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
120
+ - `sobrescribir`: Sobrescribe el archivo completo.
111
121
  - **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
112
122
 
113
123
  ```bash
124
+ # Edición precisa mediante diffs unificados
114
125
  snapcontext "añadir endpoint de métricas" --editor propio
126
+
127
+ # Forzar modo parche
128
+ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
129
+
130
+ # Refactorización estructural con AST
131
+ snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
115
132
  ```
116
133
 
117
134
  ## 🧭 Modos y alias
@@ -1,6 +1,6 @@
1
1
  # SnapContext
2
2
 
3
- ![v2.0.0](https://img.shields.io/badge/version-2.0.0-blue.svg)
3
+ ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
4
4
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
6
6
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -59,14 +59,31 @@ snapcontext interactive # o snapcontext --web
59
59
  Auto-detección de proyecto (Flutter, Node/React, Python, Go, Rust…) que ajusta
60
60
  carpetas, extensiones y comandos de test por defecto.
61
61
 
62
- ## 🛠️ Editor Integrado Propio (v2.0.0)
62
+ ## 🛠️ Editor Integrado Propio (v2.2.0)
63
63
 
64
- SnapContext v2.0.0 incluye su propio editor integrado:
64
+ SnapContext incluye su propio editor integrado con soporte de parches unificados
65
+ y edición basada en AST (Árbol Sintáctico) para refactorizaciones precisas:
65
66
  - **`--editor propio`**: Aplica cambios directamente y crea copias de seguridad automáticas en `~/.snapcontext/backups/`.
67
+ - **`--modo-edicion {auto,parche,sobrescribir,ast}`**:
68
+ - `auto` (por defecto): Genera un parche unificado (`git apply` / `patch`), pero
69
+ para refactorizaciones estructurales intenta primero la edición **AST** y cae a
70
+ sobrescritura si nada es aplicable.
71
+ - `ast`: Edita comprendiendo la estructura sintáctica del archivo (Python con
72
+ `ast`, otros lenguajes con `tree-sitter` si está instalado). Soporta renombrar
73
+ símbolos, insertar imports y recibir el código completo resultante.
74
+ - `parche`: Fuerza la aplicación de parches unificados para ediciones precisas.
75
+ - `sobrescribir`: Sobrescribe el archivo completo.
66
76
  - **`--editor aider`** (por defecto): Mantiene el flujo de edición con Aider para máxima compatibilidad.
67
77
 
68
78
  ```bash
79
+ # Edición precisa mediante diffs unificados
69
80
  snapcontext "añadir endpoint de métricas" --editor propio
81
+
82
+ # Forzar modo parche
83
+ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
84
+
85
+ # Refactorización estructural con AST
86
+ snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
70
87
  ```
71
88
 
72
89
  ## 🧭 Modos y alias
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Módulo de Agentes de SnapContext.
5
+
6
+ Arquitectura de agentes (cada agente se centra en una responsabilidad del
7
+ pipeline y se coordina desde el Orquestador):
8
+
9
+ - ``AgenteContexto``: analiza el repositorio y selecciona los archivos
10
+ relevantes para la tarea. Es el paso "Contexto" (escaneo + selección IA).
11
+ - ``AgenteEditor`` : recibe órdenes EXACTAS de qué cambiar y ejecuta Aider
12
+ sobre los archivos seleccionados. No decide qué cambiar; solo aplica órdenes.
13
+ - ``AgenteTester`` : ejecuta las pruebas y analiza el error devuelto para
14
+ que el Editor corrija justo lo que falló.
15
+
16
+ Cada método importa de forma diferida las funciones ya probadas de
17
+ ``snapcontext``, para conservar exactamente el comportamiento actual, y añade
18
+ logs de depuración visibles con ``--depurar`` (marcas ``[Agente…]``).
19
+ """
20
+
21
+ import subprocess
22
+ from typing import List, Optional, Union
23
+
24
+ def _tarea_estructura(tarea: str) -> bool:
25
+ """Heurística simple para `auto`: ¿la tarea parece una refactorización estructural?
26
+
27
+ Si es así, merece la pena intentar primero la edición basada en AST.
28
+ Se normalizan acentos para tolerar variaciones ("renombra"/"renombrar", etc.).
29
+ """
30
+ if not tarea:
31
+ return False
32
+ import unicodedata as _u
33
+
34
+ t = _u.normalize("NFD", tarea.lower())
35
+ t = "".join(c for c in t if not _u.combining(c))
36
+ claves = ("refactor", "renombra", "renombrar", "extra", "extraer",
37
+ "mueve", "mover", "inserta", "insertar", "nueva funcion",
38
+ "crear funcion", "funcion", "extraccion")
39
+ return any(k in t for k in claves)
40
+
41
+ class AgenteContexto:
42
+ """Agente de Contexto: selecciona los archivos relevantes para la consulta.
43
+
44
+ No modifica código: solo elige qué archivos merece la pena abrir/editar.
45
+ Compone el escaneo local (heurística) y la selección final con el proveedor
46
+ de IA configurado.
47
+ """
48
+
49
+ def escanear_candidatos(
50
+ self,
51
+ consulta: str,
52
+ directorio: str = ".",
53
+ carpetas: Optional[List[str]] = None,
54
+ extensiones: Optional[List[str]] = None,
55
+ max_candidatos: int = 80,
56
+ ) -> List[str]:
57
+ """Escanea ``directorio`` y devuelve los ``max_candidatos`` archivos más
58
+ relevantes para ``consulta`` usando heurística local (sin llamar a la IA).
59
+
60
+ ``extensiones`` (opcional) restringe el escaneo a determinadas
61
+ extensiones (p. ej. ``[\".dart\"]``).
62
+
63
+ Este es el sub-paso 1 del pipeline: reduce el repositorio a candidatos.
64
+ """
65
+ import snapcontext as sc
66
+
67
+ sc.depurar(
68
+ f"[AgenteContexto] Escaneando '{directorio}' "
69
+ f"(carpetas={carpetas}, extensiones={extensiones}, "
70
+ f"max_candidatos={max_candidatos})..."
71
+ )
72
+ resultado = sc.escanear_repositorio(
73
+ consulta,
74
+ directorio=directorio,
75
+ carpetas=carpetas,
76
+ extensiones=extensiones,
77
+ max_candidatos=max_candidatos,
78
+ )
79
+ sc.depurar(
80
+ f"[AgenteContexto] {len(resultado)} candidato(s) relevante(s) localmente."
81
+ )
82
+ return resultado
83
+
84
+ def seleccionar_archivos(
85
+ self,
86
+ consulta: str,
87
+ directorio: str,
88
+ carpetas: Optional[List[str]],
89
+ max_archivos: int,
90
+ provider: str,
91
+ modelo: str,
92
+ extensiones: Optional[List[str]] = None,
93
+ ) -> List[str]:
94
+ """Pipeline completo del agente de contexto: escanea el repositorio y
95
+ pide al proveedor IA ``provider`` que se quede con las ``max_archivos``
96
+ rutas más relevantes para ``consulta``.
97
+
98
+ Devuelve la lista final (vacía si no hay candidatos).
99
+ """
100
+ import snapcontext as sc
101
+
102
+ candidatos = self.escanear_candidatos(
103
+ consulta,
104
+ directorio=directorio,
105
+ carpetas=carpetas,
106
+ extensiones=extensiones,
107
+ max_candidatos=max(max_archivos * 3, 1),
108
+ )
109
+ if not candidatos:
110
+ sc.depurar("[AgenteContexto] Sin candidatos; no hay qué seleccionar.")
111
+ return []
112
+
113
+ sc.depurar(
114
+ f"[AgenteContexto] Seleccionando {max_archivos} rutas "
115
+ f"(proveedor='{provider}', modelo='{modelo}')..."
116
+ )
117
+ return sc.seleccionar_archivos(
118
+ consulta,
119
+ candidatos,
120
+ proveedor=provider,
121
+ modelo=modelo,
122
+ max_archivos=max_archivos,
123
+ )
124
+
125
+ # __PARTE2__
126
+ class AgenteEditor:
127
+ """Agente Editor: recibe órdenes exactas y ejecuta Aider.
128
+
129
+ No decide de forma autónoma qué cambiar: aplica el ``mensaje`` (normalmente
130
+ la tarea original y, si viene del ``AgenteTester``, el error exacto) sobre
131
+ los ``archivos`` indicados por el orquestador.
132
+ """
133
+
134
+ def ejecutar_aider(
135
+ self,
136
+ archivos: List[str],
137
+ mensaje: str,
138
+ directorio: str,
139
+ opciones: str = "",
140
+ ) -> bool:
141
+ """Ejecuta Aider en ``directorio`` sobre ``archivos`` con ``mensaje``.
142
+
143
+ Devuelve ``True`` si Aider terminó con código 0; ``False`` en otro caso.
144
+ """
145
+ import snapcontext as sc
146
+
147
+ sc.depurar(
148
+ f"[AgenteEditor] Aider sobre {len(archivos)} archivo(s) en '{directorio}'"
149
+ )
150
+ return sc.ejecutar_aider(archivos, mensaje, directorio, opciones)
151
+
152
+
153
+ class AgenteEditorPropio:
154
+ """Agente Editor Propio: editor integrado de SnapContext.
155
+
156
+ Aplica cambios directamente sobre el sistema de archivos sin depender de
157
+ herramientas externas obligatorias.
158
+ - Fase 1: Sobrescritura segura con copia de seguridad previa en ``~/.snapcontext/backups/``.
159
+ - Fase 2: Generación y aplicación de parches unificados (unified diffs) con ``git apply`` / ``patch``
160
+ y fallback automático a sobrescritura.
161
+ """
162
+
163
+ def sobrescribir(
164
+ self,
165
+ archivo: str,
166
+ contenido: str,
167
+ directorio: str = ".",
168
+ ) -> bool:
169
+ """Sobrescribe ``archivo`` con ``contenido`` en ``directorio``.
170
+
171
+ Devuelve ``True`` si la escritura fue exitosa, ``False`` si falló.
172
+ """
173
+ import snapcontext as sc
174
+
175
+ sc.depurar(
176
+ f"[AgenteEditorPropio] Sobrescribiendo '{archivo}' en '{directorio}'"
177
+ )
178
+ return sc._editor_sobrescribir(archivo, contenido, directorio=directorio)
179
+
180
+ def aplicar_parche(
181
+ self,
182
+ parche: str,
183
+ directorio: str = ".",
184
+ ) -> bool:
185
+ """Aplica un parche unificado en ``directorio``.
186
+
187
+ Devuelve ``True`` si se aplicó con éxito, ``False`` si falló.
188
+ """
189
+ import snapcontext as sc
190
+
191
+ sc.depurar(f"[AgenteEditorPropio] Aplicando parche en '{directorio}'")
192
+ return sc._aplicar_parche(parche, directorio=directorio)
193
+
194
+ def _cadena_modos(self, archivo: str, mensaje: str,
195
+ modo_edicion: str) -> List[str]:
196
+ """Devuelve la cadena de estrategias de edición a intentar para un archivo.
197
+
198
+ - 'sobrescribir' → solo sobrescritura.
199
+ - 'parche' → solo parche unificado (sin fallback).
200
+ - 'ast' → AST y, si falla, sobrescritura.
201
+ - 'auto' → AST (si el lenguaje lo permite y la tarea es estructural),
202
+ luego parche y, por último, sobrescritura.
203
+ """
204
+ import snapcontext as sc
205
+
206
+ if modo_edicion == "sobrescribir":
207
+ return ["sobrescribir"]
208
+ if modo_edicion == "parche":
209
+ return ["parche"]
210
+ if modo_edicion == "ast":
211
+ return ["ast", "sobrescribir"]
212
+ # auto: heurística simple según lenguaje y tarea
213
+ if sc._ast_disponible(archivo) and _tarea_estructura(mensaje):
214
+ return ["ast", "parche", "sobrescribir"]
215
+ return ["parche", "sobrescribir"]
216
+
217
+ def editar_ast(self, archivo: str, tarea: str,
218
+ directorio: str = ".", modelo: Optional[str] = None) -> bool:
219
+ """Edita ``archivo`` con base en su AST usando el proveedor de IA."""
220
+ import snapcontext as sc
221
+
222
+ pref = sc.cargar_configuracion()
223
+ proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
224
+ sc.depurar(f"[AgenteEditorPropio] Editando por AST '{archivo}' en '{directorio}'")
225
+ return sc._editor_ast(archivo, tarea, directorio=directorio,
226
+ proveedor=proveedor, modelo=modelo)
227
+
228
+ def _aplicar_modo_parche(self, archivo: str, mensaje: str,
229
+ contenido_actual: str, modelo: Optional[str],
230
+ directorio: str) -> bool:
231
+ """Intenta editar `archivo` pidiendo un parche unificado al proveedor."""
232
+ import snapcontext as sc
233
+
234
+ pref = sc.cargar_configuracion()
235
+ proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
236
+ prompt = (
237
+ f"Genera un parche unificado (unified diff) que modifique el archivo para cumplir con la tarea. "
238
+ f"El parche debe ser aplicable con 'patch -p1' o 'git apply' (encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
239
+ f"Tarea: {mensaje}\n"
240
+ f"Archivo: {archivo}\n\n"
241
+ f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
242
+ f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
243
+ )
244
+ respuesta = sc._enviar_al_proveedor(
245
+ proveedor, modelo, [{"role": "user", "content": prompt}])
246
+ diff_limpio = respuesta
247
+ if "--- " in diff_limpio and "+++ " in diff_limpio:
248
+ idx_inicio = diff_limpio.find("--- ")
249
+ diff_limpio = diff_limpio[idx_inicio:]
250
+ if "```" in diff_limpio:
251
+ diff_limpio = diff_limpio[:diff_limpio.find("```")]
252
+ if "--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio:
253
+ return self.aplicar_parche(diff_limpio, directorio)
254
+ return False
255
+
256
+ def _aplicar_modo_sobrescribir(self, archivo: str, mensaje: str,
257
+ contenido_actual: str, modelo: Optional[str],
258
+ directorio: str) -> bool:
259
+ """Sobrescribe `archivo` con el código completo que devuelve el proveedor."""
260
+ import snapcontext as sc
261
+
262
+ pref = sc.cargar_configuracion()
263
+ proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
264
+ prompt = (
265
+ f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
266
+ f"Tarea: {mensaje}\n"
267
+ f"Archivo: {archivo}\n\n"
268
+ f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
269
+ f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
270
+ )
271
+ nuevo_contenido = sc._enviar_al_proveedor(
272
+ proveedor, modelo, [{"role": "user", "content": prompt}])
273
+ if nuevo_contenido.startswith("```"):
274
+ lineas = nuevo_contenido.splitlines()
275
+ if len(lineas) >= 2 and lineas[-1].startswith("```"):
276
+ nuevo_contenido = "\n".join(lineas[1:-1])
277
+ return self.sobrescribir(archivo, nuevo_contenido, directorio)
278
+
279
+ def ejecutar(
280
+ self,
281
+ archivos: List[str],
282
+ mensaje: str,
283
+ directorio: str = ".",
284
+ modo_edicion: str = "auto",
285
+ modelo: Optional[str] = None,
286
+ ) -> bool:
287
+ """Aplica las modificaciones para `archivos` con el `mensaje` especificado.
288
+
289
+ - modo_edicion == 'parche': intenta aplicar parche unificado (falla si no puede).
290
+ - modo_edicion == 'sobrescribir': aplica directamente sobrescritura de archivo.
291
+ - modo_edicion == 'ast': edita con base en el AST y cae a sobrescritura si falla.
292
+ - modo_edicion == 'auto': decide por heurística (AST para refactorizaciones,
293
+ parche, y fallback a sobrescritura).
294
+ """
295
+ import snapcontext as sc
296
+ from pathlib import Path
297
+
298
+ raiz = Path(directorio).resolve()
299
+ todo_ok = True
300
+
301
+ for arch in archivos:
302
+ camino = (raiz / arch).resolve()
303
+ contenido_actual = ""
304
+ if camino.is_file():
305
+ try:
306
+ contenido_actual = camino.read_text(encoding="utf-8",
307
+ errors="replace")
308
+ except Exception:
309
+ pass
310
+
311
+ cadena = self._cadena_modos(arch, mensaje, modo_edicion)
312
+ conseguido = False
313
+ for estrategia in cadena:
314
+ try:
315
+ if estrategia == "ast":
316
+ conseguido = self.editar_ast(arch, mensaje,
317
+ str(raiz), modelo)
318
+ elif estrategia == "parche":
319
+ conseguido = self._aplicar_modo_parche(
320
+ arch, mensaje, contenido_actual, modelo, str(raiz))
321
+ elif estrategia == "sobrescribir":
322
+ conseguido = self._aplicar_modo_sobrescribir(
323
+ arch, mensaje, contenido_actual, modelo, str(raiz))
324
+ else:
325
+ conseguido = False
326
+ except Exception as exc:
327
+ sc.depurar(
328
+ f"[AgenteEditorPropio] {estrategia} falló para '{arch}': {exc}")
329
+ conseguido = False
330
+ if conseguido:
331
+ break
332
+ if not conseguido:
333
+ sc.error(f"[AgenteEditorPropio] No se pudo editar '{arch}'.")
334
+ todo_ok = False
335
+
336
+ return todo_ok
337
+
338
+
339
+ class AgenteEditorAST:
340
+ """Agente Editor AST (Fase 3): ediciones precisas basadas en árbol sintáctico.
341
+
342
+ Trabaja directamente con el AST del archivo (Python con ``ast``; otros
343
+ lenguajes con ``tree-sitter`` si está instalado) pidiendo al proveedor
344
+ operaciones de modificación del árbol o el código completo resultante.
345
+ """
346
+
347
+ def editar(
348
+ self,
349
+ archivos: List[str],
350
+ mensaje: str,
351
+ directorio: str = ".",
352
+ modelo: Optional[str] = None,
353
+ ) -> bool:
354
+ """Aplica a cada archivo una edición guiada por AST. Devuelve bool."""
355
+ todo = True
356
+ for archivo in archivos:
357
+ if not self.editar_archivo(archivo, mensaje,
358
+ directorio=directorio, modelo=modelo):
359
+ todo = False
360
+ return todo
361
+
362
+ def editar_archivo(
363
+ self,
364
+ archivo: str,
365
+ tarea: str,
366
+ directorio: str = ".",
367
+ modelo: Optional[str] = None,
368
+ ) -> bool:
369
+ """Edita un único archivo con AST. Devuelve ``True`` si tuvo éxito."""
370
+ import snapcontext as sc
371
+
372
+ pref = sc.cargar_configuracion()
373
+ proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
374
+ sc.depurar(f"[AgenteEditorAST] AST sobre '{archivo}' en '{directorio}'")
375
+ return sc._editor_ast(archivo, tarea, directorio=directorio,
376
+ proveedor=proveedor, modelo=modelo)
377
+
378
+
379
+ class AgenteTester:
380
+ """Agente Tester: ejecuta las pruebas y analiza los errores devueltos.
381
+
382
+ Su salida alimenta al ``AgenteEditor`` para que corrija justo lo que falló,
383
+ sin que Aider pierda de vista la tarea original.
384
+ """
385
+
386
+ def ejecutar_pruebas(
387
+ self, comando: List[str], directorio: str
388
+ ) -> "subprocess.CompletedProcess":
389
+ """Ejecuta ``comando`` (lista de argv) en ``directorio`` capturando la salida.
390
+
391
+ Devuelve el ``subprocess.CompletedProcess`` para poder analizarlo después.
392
+ """
393
+ import snapcontext as sc
394
+
395
+ sc.depurar(f"[AgenteTester] Ejecutando pruebas: {' '.join(comando)}")
396
+ return subprocess.run(
397
+ comando, cwd=directorio, capture_output=True, text=True
398
+ )
399
+
400
+ def analizar_error(
401
+ self, salida: Union["subprocess.CompletedProcess", str]
402
+ ) -> str:
403
+ """Normaliza la salida de unas pruebas que fallaron para entregársela a Aider.
404
+
405
+ Acepta el ``CompletedProcess`` devuelto por :meth:`ejecutar_pruebas` o un
406
+ ``str`` con la salida cruda. Limpia códigos ANSI y recorta demasiado
407
+ texto para no llenar el contexto de Aider.
408
+ """
409
+ import snapcontext as sc
410
+
411
+ if hasattr(salida, "stdout"):
412
+ # Objeto CompletedProcess → reutilizamos el normalizador de snapcontext.
413
+ return sc._extraer_error(salida)
414
+
415
+ limpieza: str = salida or ""
416
+ import re as _re
417
+
418
+ limpieza = _re.sub(r"\x1b\[[0-9;]*m", "", limpieza)
419
+ limpieza = limpieza.strip() or "(el comando de prueba no devolvió salida)"
420
+ limite = getattr(sc, "MAX_ERROR_SALIDA", 4000)
421
+ if len(limpieza) > limite:
422
+ limpieza = "\n--- (salida recortada) ---\n" + limpieza[-limite:]
423
+ return limpieza
@@ -20,7 +20,8 @@ import shlex
20
20
  import sys
21
21
  from typing import List, Optional, Tuple
22
22
 
23
- from agentes import AgenteContexto, AgenteEditor, AgenteEditorPropio, AgenteTester
23
+ from agentes import (AgenteContexto, AgenteEditor, AgenteEditorAST,
24
+ AgenteEditorPropio, AgenteTester)
24
25
 
25
26
  # Centinela diferenciado del None de "aborto": _planificar lo devuelve cuando
26
27
  # el pipeline termina de forma exitosa y temprana (p. ej. --vista-previa).
@@ -40,6 +41,7 @@ class Orquestador:
40
41
  self.agente_contexto = AgenteContexto()
41
42
  self.agente_editor = AgenteEditor()
42
43
  self.agente_editor_propio = AgenteEditorPropio()
44
+ self.agente_editor_ast = AgenteEditorAST()
43
45
  self.agente_tester = AgenteTester()
44
46
  self.evento_callback = evento_callback
45
47
 
@@ -180,38 +182,14 @@ class Orquestador:
180
182
  if editor_tipo == "propio":
181
183
  sc.depurar("[Orquestador] Modo edición directa (AgenteEditorPropio).")
182
184
  self._emitir_tipo("editor", accion="iniciar", archivos=seleccion)
183
- pref = sc.cargar_configuracion()
184
- proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
185
- ok = True
186
- for arch in seleccion:
187
- camino = (raiz / arch).resolve()
188
- contenido_actual = ""
189
- if camino.is_file():
190
- try:
191
- contenido_actual = camino.read_text(encoding="utf-8", errors="replace")
192
- except Exception:
193
- pass
194
- prompt_editor = (
195
- f"Modifica el siguiente archivo para cumplir con la tarea.\n\n"
196
- f"Tarea: {consulta}\n"
197
- f"Archivo: {arch}\n\n"
198
- f"Contenido actual:\n```\n{contenido_actual}\n```\n\n"
199
- f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
200
- )
201
- try:
202
- nuevo_contenido = sc._enviar_al_proveedor(
203
- proveedor, getattr(args, "modelo", None),
204
- [{"role": "user", "content": prompt_editor}]
205
- )
206
- if nuevo_contenido.startswith("```"):
207
- lineas = nuevo_contenido.splitlines()
208
- if len(lineas) >= 2 and lineas[-1].startswith("```"):
209
- nuevo_contenido = "\n".join(lineas[1:-1])
210
- if not self.agente_editor_propio.sobrescribir(arch, nuevo_contenido, str(raiz)):
211
- ok = False
212
- except Exception as exc:
213
- sc.error(f"Error generando cambios para {arch}: {exc}")
214
- ok = False
185
+ modo_ed = getattr(args, "modo_edicion", "auto") or "auto"
186
+ ok = self.agente_editor_propio.ejecutar(
187
+ seleccion,
188
+ consulta,
189
+ directorio=str(raiz),
190
+ modo_edicion=modo_ed,
191
+ modelo=getattr(args, "modelo", None),
192
+ )
215
193
  self._emitir_tipo("editor", accion="fin", ok=ok)
216
194
  else:
217
195
  sc.depurar("[Orquestador] Modo edición directa (AgenteEditor).")