xonecode 0.17.2 → 0.20.1

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 (115) hide show
  1. package/apps/web/dist/assets/{index-BRVuyPBf.js → index-BvPUqjUm.js} +72 -72
  2. package/apps/web/dist/assets/{index-DLfF2uEK.css → index-CGEXMJBx.css} +1 -1
  3. package/apps/web/dist/assets/{php-B5LpMCn-.js → php-Dk28VW45.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-Clm-qfwM.js → ruby-CPzEcmdp.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/config/iconosEnRed.js +47 -0
  7. package/dist/agent/config/iconosEnRed.js.map +1 -0
  8. package/dist/agent/config/modelos.js +13 -8
  9. package/dist/agent/config/modelos.js.map +1 -1
  10. package/dist/agent/dispositivos/decodificarImagen.js +62 -0
  11. package/dist/agent/dispositivos/decodificarImagen.js.map +1 -0
  12. package/dist/agent/grafo/atributosXone.js +58 -0
  13. package/dist/agent/grafo/atributosXone.js.map +1 -0
  14. package/dist/agent/grafo/buscarIcono.js +92 -0
  15. package/dist/agent/grafo/buscarIcono.js.map +1 -0
  16. package/dist/agent/grafo/compararCapturas.js +103 -0
  17. package/dist/agent/grafo/compararCapturas.js.map +1 -0
  18. package/dist/agent/grafo/criticaVisual.js +23 -3
  19. package/dist/agent/grafo/criticaVisual.js.map +1 -1
  20. package/dist/agent/grafo/diferenciaDeCapturas.js +88 -0
  21. package/dist/agent/grafo/diferenciaDeCapturas.js.map +1 -0
  22. package/dist/agent/grafo/generarFondoSvg.js +83 -0
  23. package/dist/agent/grafo/generarFondoSvg.js.map +1 -0
  24. package/dist/agent/grafo/lectorDeReferencias.js +19 -0
  25. package/dist/agent/grafo/lectorDeReferencias.js.map +1 -0
  26. package/dist/agent/grafo/xoneAgent.js +11 -3
  27. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  28. package/dist/agent/motores/trueforge/capacidades.js +49 -1
  29. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  30. package/dist/agent/motores/trueforge/esperas.js +61 -0
  31. package/dist/agent/motores/trueforge/esperas.js.map +1 -0
  32. package/dist/agent/motores/trueforge/eventosTrueforge.js +11 -1
  33. package/dist/agent/motores/trueforge/eventosTrueforge.js.map +1 -1
  34. package/dist/agent/motores/trueforge/informesDeHijos.js +63 -0
  35. package/dist/agent/motores/trueforge/informesDeHijos.js.map +1 -0
  36. package/dist/agent/motores/trueforge/medidaAutomatica.js +118 -0
  37. package/dist/agent/motores/trueforge/medidaAutomatica.js.map +1 -0
  38. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js +186 -0
  39. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js.map +1 -0
  40. package/dist/agent/motores/trueforge/modeloLangchain.js +5 -0
  41. package/dist/agent/motores/trueforge/modeloLangchain.js.map +1 -1
  42. package/dist/agent/motores/trueforge/resumenSeguro.js +52 -0
  43. package/dist/agent/motores/trueforge/resumenSeguro.js.map +1 -0
  44. package/dist/agent/motores/trueforge/sesionTrueforge.js +545 -26
  45. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  46. package/dist/agent/motores/trueforge/toolsDeFichero.js +69 -3
  47. package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -1
  48. package/dist/agent/subagentes/agentesEnDisco.js +88 -19
  49. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  50. package/dist/agent/turno/diagnosticoDeTools.js +44 -3
  51. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
  52. package/dist/agent/turno/informeDeTraza.js +66 -4
  53. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  54. package/dist/agent/turno/registroDeFallos.js +6 -1
  55. package/dist/agent/turno/registroDeFallos.js.map +1 -1
  56. package/dist/agent/turno/resumenDeTool.js +12 -0
  57. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  58. package/dist/agent/turno/turnoReal.js +8 -2
  59. package/dist/agent/turno/turnoReal.js.map +1 -1
  60. package/dist/cli/run.js +11 -0
  61. package/dist/cli/run.js.map +1 -1
  62. package/dist/core/agentes.js +36 -2
  63. package/dist/core/agentes.js.map +1 -1
  64. package/dist/core/analisisDeSesion.js +198 -0
  65. package/dist/core/analisisDeSesion.js.map +1 -0
  66. package/dist/core/atributosXone.js +187 -0
  67. package/dist/core/atributosXone.js.map +1 -0
  68. package/dist/core/compararCapturas.js +405 -0
  69. package/dist/core/compararCapturas.js.map +1 -0
  70. package/dist/core/esfuerzo.js +33 -0
  71. package/dist/core/esfuerzo.js.map +1 -1
  72. package/dist/core/fallos.js.map +1 -1
  73. package/dist/core/fondosSvg.js +223 -0
  74. package/dist/core/fondosSvg.js.map +1 -0
  75. package/dist/core/iconos.js +140 -0
  76. package/dist/core/iconos.js.map +1 -0
  77. package/dist/core/modoDeEscritura.js +40 -0
  78. package/dist/core/modoDeEscritura.js.map +1 -1
  79. package/dist/core/notify.js +8 -0
  80. package/dist/core/notify.js.map +1 -1
  81. package/dist/core/nucleoXone.js +113 -0
  82. package/dist/core/nucleoXone.js.map +1 -0
  83. package/dist/core/paqueteDeSoporte.js +118 -0
  84. package/dist/core/paqueteDeSoporte.js.map +1 -0
  85. package/dist/core/ports.js +31 -0
  86. package/dist/core/ports.js.map +1 -1
  87. package/dist/core/referenciasDeImagen.js +55 -0
  88. package/dist/core/referenciasDeImagen.js.map +1 -0
  89. package/dist/core/selectorDeCarpeta.js +15 -1
  90. package/dist/core/selectorDeCarpeta.js.map +1 -1
  91. package/dist/core/settings.js +19 -4
  92. package/dist/core/settings.js.map +1 -1
  93. package/dist/core/shellDeAgente.js +39 -0
  94. package/dist/core/shellDeAgente.js.map +1 -1
  95. package/dist/core/tareasDelPlan.js +69 -7
  96. package/dist/core/tareasDelPlan.js.map +1 -1
  97. package/dist/web/servidor/arranque.js +110 -0
  98. package/dist/web/servidor/arranque.js.map +1 -1
  99. package/dist/web/servidor/sesiones.js +8 -0
  100. package/dist/web/servidor/sesiones.js.map +1 -1
  101. package/dist/web/servidor/soporte.js +571 -0
  102. package/dist/web/servidor/soporte.js.map +1 -0
  103. package/dist/web/servidor/transporte.js.map +1 -1
  104. package/package.json +4 -1
  105. package/skills/xone-development/references/css/propiedades-y-herencia.md +2 -0
  106. package/skills/xone-development/references/datos/appdata-referencia-ampliada.md +2 -2
  107. package/skills/xone-development/references/datos/appdata.md +5 -3
  108. package/skills/xone-development/references/fundamentos/errores-comunes.md +49 -0
  109. package/skills/xone-development/references/xml-ui/prop-tipos-basicos.md +2 -0
  110. package/skills/xone-hotswap/SKILL.md +4 -0
  111. package/skills/xone-hotswap/scripts/xone-log-android +18 -1
  112. package/skills/xone-plan-builder/SKILL.md +18 -10
  113. package/skills/xone-plan-builder/references/TASKS-FORMAT.md +13 -5
  114. package/skills/xone-project-generator/references/fases-10-12-readmes-y-validacion.md +1 -0
  115. package/skills/xone-spec-builder/SKILL.md +3 -3
@@ -23,7 +23,9 @@
23
23
  * (`modeloExterno.ts`).
24
24
  * - NO: un presupuesto GLOBAL por turno (la suma de todos los hilos), que deepagents tampoco tiene.
25
25
  */
26
- import { readFileSync } from "node:fs";
26
+ import { TOPE_DE_PREGUNTAS_CONTESTADAS_SOLAS, opcionRecomendada } from "../../../core/modoDeEscritura.js";
27
+ import { claseDeTrabajo } from "../../../core/esfuerzo.js";
28
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
27
29
  import { join } from "node:path";
28
30
  import winston from "winston";
29
31
  import { AgentThread, AgentThreadOrchestrator, EventType, NOOP_AGENT_TRACING, askUserQuestion, contextCompaction, dynamicSubAgents } from "./trueforge.js";
@@ -51,7 +53,9 @@ import { modeloParaTrueforge } from "./modeloLangchain.js";
51
53
  import { TOOLS_DE_LECTURA } from "./toolsDeFichero.js";
52
54
  import { topeAgotadoDe, traducirEvento } from "./eventosTrueforge.js";
53
55
  import { anuncioDeSkills } from "./skillsTrueforge.js";
54
- import { capabilitiesDe, capacidadDeFecha, capacidadDeFicheros, capacidadDeInstrucciones, capacidadDeNotas, capacidadDePropias, capacidadDeRecortes, capacidadesDelEspecialista, clasesDeTools, toolsDe, } from "./capacidades.js";
56
+ import { capabilitiesDe, capacidadDeFecha, capacidadDeFicheros, capacidadDeAvisoDeVueltas, capacidadDeInstrucciones, capacidadDeNotas, capacidadDePropias, capacidadDeRecortes, capacidadesDelEspecialista, clasesDeTools, toolsDe, } from "./capacidades.js";
57
+ import { buscarMaqueta, medirContraMaqueta, resumenDeMedida, textoDeMedidaAutomatica, ultimaCaptura } from "./medidaAutomatica.js";
58
+ import { anotarEscritura, capacidadDeInformesDeHijos, escrituraConExito, textoDelInforme } from "./informesDeHijos.js";
55
59
  import { crearDiagnosticoDeTools } from "../../turno/diagnosticoDeTools.js";
56
60
  import { encenderTrazaDeErrores } from "../../trazaDeErroresEnDisco.js";
57
61
  import { anotarPaso } from "../../../core/trazaDeErrores.js";
@@ -59,28 +63,213 @@ import { entornoConDepuracion } from "../../turno/depuracion.js";
59
63
  import { detalleDe, parametrosDe } from "../../turno/resumenDeTool.js";
60
64
  import { apartarMemoria, cargarMemoria, fotoSaneada, guardarMemoria, textoDeMemoriaDescartada } from "./memoriaTrueforge.js";
61
65
  import { crearNota, sobrantes } from "./notas.js";
66
+ import { crearMemoriaDeEspecialistas } from "./memoriaDeEspecialistas.js";
67
+ import { crearEsperas } from "./esperas.js";
68
+ import { conResumenSeguro } from "./resumenSeguro.js";
62
69
  import { crearControlDeDetencion, RESUMEN_DE_RELLENO } from "./detencion.js";
63
70
  import { crearNavegacionXone } from "../../grafo/navegacionXone.js";
64
71
  import { hechosDelProyectoDe } from "../../navegacion/hechosEnDisco.js";
65
72
  import { conHechosDelProyecto } from "../../../core/hechosDelProyecto.js";
66
73
  import { crearBusquedaRegex } from "../../grafo/busquedaRegex.js";
74
+ import { crearBuscarIcono, recibeBuscarIcono } from "../../grafo/buscarIcono.js";
75
+ import { crearGenerarFondoSvg, recibeGenerarFondo } from "../../grafo/generarFondoSvg.js";
67
76
  import { crearCopiarArtefacto } from "../../grafo/copiarArtefacto.js";
68
77
  import { crearMarcarCriteriosDelPlan } from "../../grafo/marcarCriteriosDelPlan.js";
69
78
  import { crearUnirSecciones } from "../../grafo/unirSecciones.js";
70
79
  import { crearIncorporarAdjunto, recibeIncorporarAdjunto } from "../../grafo/incorporarAdjunto.js";
71
80
  import { crearCriticaVisual } from "../../grafo/criticaVisual.js";
81
+ import { crearCompararCapturas } from "../../grafo/compararCapturas.js";
82
+ import { crearAtributosXone, NOMBRE_ATRIBUTOS_XONE } from "../../grafo/atributosXone.js";
83
+ import { crearDiferenciaDeCapturas } from "../../grafo/diferenciaDeCapturas.js";
84
+ import { crearLectorDeReferencias } from "../../grafo/lectorDeReferencias.js";
72
85
  import { crearTraerDeLaMaquina } from "../../grafo/traerDeLaMaquina.js";
73
86
  import { invocarVisualConModelos } from "../../dispositivos/juezVisual.js";
74
87
  import { estilosDeDisco, indiceEnDisco } from "../../navegacion/indiceEnDisco.js";
75
88
  /** El hilo raíz de TrueForge. Se llama así en la librería y no se elige. */
76
89
  const HILO_RAIZ = "main";
90
+ /**
91
+ * **Ser fiel a un diseño es parte del encargo, y se comprueba con números Y con ojos.**
92
+ *
93
+ * Rehaciendo una calculadora de Stitch, el orquestador dio la pantalla por buena una y otra vez con
94
+ * el crítico en verde, porque corría SIN la maqueta: su verde significaba «nada roto» y no veía que
95
+ * faltaba la barra superior. Con la maqueta a mano (`/diseno/`, `/adjuntos/`), el bucle es este, y
96
+ * termina: una condición de parada de estructura y un tope de vueltas, porque sin tope un crítico
97
+ * y un agente pueden retocar para siempre.
98
+ */
99
+ const BUCLE_DE_CALIDAD = [
100
+ "SI EL ENCARGO TRAE UN DISEÑO (una imagen en /diseno/, /adjuntos/ o /artefactos/), SER FIEL A ÉL es parte del",
101
+ "encargo, y se comprueba con números y con ojos. Tras cada cambio visual: pide al conductor una captura NATIVA",
102
+ "nueva y contrástala con `comparar_capturas` y con `xone_critica_visual`, pasando el diseño como `referencia` en las",
103
+ "dos. Sin referencia el crítico solo dice «nada roto», que no es «se parece». Si la estructura o el crítico señalan",
104
+ "diferencias con el diseño —piezas que faltan, tamaños, formas—, delega el arreglo en quien MAQUETA la pantalla (el que escribe su `.xne`, no el de los recursos) con esas diferencias concretas y",
105
+ "repite. Para cuando la distancia estructural baje del 10 % en vertical y en horizontal y el crítico no señale diferencias de forma con el diseño, o tras TRES vueltas, y",
106
+ "di lo que sigue faltando en vez de darlo por hecho.",
107
+ ].join("\n");
77
108
  /**
78
109
  * Lo que el orquestador tiene que saber para DELEGAR en ESTE motor. Su prompt es el de siempre
79
110
  * (`promptOrquestador`), que habla de `task` y de la ficha que va en su descripción: aquí la
80
111
  * delegación es `create_sub_agent` y no tiene descripción por especialista, así que se traduce
81
112
  * el nombre de la tool y las fichas van escritas aquí, con la MISMA función que deepagents.
82
113
  */
83
- export function notaDeDelegacion(agentes) {
114
+ /**
115
+ * **El idioma, dicho desde código**, al raíz y a cada especialista. Visto en la consola web (MyAllXOne): el texto entre
116
+ * herramientas y el razonamiento salían en inglés («Let me call…», «I'll start by reconnoitering…») con la persona
117
+ * escribiendo en español. Nuestros prompts están en español pero ninguno lo decía, y encima hay piezas internas en inglés
118
+ * que tiran de él: la identidad que añade TrueForge («You are the Agent…»), las descripciones de las tools y la costumbre
119
+ * de DeepSeek de razonar en inglés.
120
+ */
121
+ export const IDIOMA_DE_LA_RESPUESTA = "IDIOMA: escribe SIEMPRE en español —lo que dices entre herramientas, tu razonamiento, tus encargos y tus respuestas—, " +
122
+ "aunque algunas instrucciones internas y las descripciones de las herramientas estén en inglés. El código, los nombres de " +
123
+ "ficheros, atributos y funciones, y las citas literales se quedan como son.";
124
+ /** Lo que recibe el agente en modo autónomo cuando pregunta sin marcar ninguna opción como recomendada. */
125
+ export const RESPUESTA_AUTONOMA_SIN_RECOMENDADA = "Modo autónomo: no hay nadie a quien preguntar. Decide tú con lo que dicen el encargo y el plan, sigue, y di en tu respuesta final qué elegiste y por qué.";
126
+ /** Un comando que saca una captura de pantalla del aparato. */
127
+ export const COMANDO_DE_CAPTURA = /xone-captura-android|xone-hotswap\s+shot\b|\bscreencap\b/;
128
+ /** Con cuántas capturas de UN encargo se le avisa a quien ejecuta. */
129
+ export const UMBRALES_DE_CAPTURAS = [2, 4];
130
+ /**
131
+ * El aviso de capturas. El prompt del conductor ya decía que una captura «no es una herramienta de diagnóstico», y una
132
+ * comprobación de una pasada real sacó 18 —61 ficheros y 92 pares casi iguales en total—, casi todas para mirar valores
133
+ * y excepciones que `getText`/`getFields` y el log contestan sin mirar nada. Un texto no es un contador.
134
+ */
135
+ export function textoDeCapturas(capturas) {
136
+ return [
137
+ `LLEVAS ${String(capturas)} CAPTURAS en este encargo.`,
138
+ "Para un VALOR, un estado o una excepción no hace falta ninguna: `xone-log-android --app` (lo que la app escribió con",
139
+ "`console.log`), `getFields`, `elements` acotado y el log de errores lo dicen sin mirar la pantalla. Una captura es para lo VISUAL (algo cortado, tapado, del color o tamaño equivocado) y una por",
140
+ "cosa que compruebes, con `shot name=<qué pruebas>`. Si solo quieres saber si algo cambió, `diferencia_de_capturas`.",
141
+ "No repitas la misma pantalla: si la anterior ya lo decía, no hay nada nuevo que sacar.",
142
+ ].join("\n");
143
+ }
144
+ /** Con cuántas comprobaciones seguidas se le avisa a quien lleva un lazo. El primero es el «tres vueltas» del prompt. */
145
+ export const UMBRALES_DE_VUELTAS = [3, 5];
146
+ /**
147
+ * El aviso: no dice «para», dice «cambia de procedimiento». La misma familia de fallo se vio arreglada de cinco
148
+ * maneras (un control que falta, y al arreglarlo, otro): lo que hay que hacer es aislar la causa una vez y aplicarla
149
+ * a todos los sitios, no reescribir otra vez.
150
+ */
151
+ export function textoDeVueltas(vueltas) {
152
+ return [
153
+ `LLEVAS ${String(vueltas)} COMPROBACIONES SEGUIDAS con el aparato.`,
154
+ "Si el fallo de la última es de la MISMA FAMILIA que el anterior (la misma excepción, el mismo tipo de control), NO hagas otra",
155
+ "reescritura: (1) di la hipótesis en una frase, (2) pide al de pruebas UN experimento mínimo —un control, un evento— y lee",
156
+ "el resultado BRUTO del aparato, (3) con la causa confirmada, aplica el arreglo a TODOS los sitios parecidos de una vez.",
157
+ "Si tras eso el fallo sigue igual, devuelve el trabajo diciendo qué falta y qué mediste, en vez de otra vuelta.",
158
+ ].join("\n");
159
+ }
160
+ /**
161
+ * El hilo de un especialista que NO arrancó porque espera a otros: nace ya terminado, con la respuesta al orquestador
162
+ * ya escrita (`preComputedCompletion`), y la librería lo trata como cualquier hijo que acaba. No llama al modelo ni tiene
163
+ * tools: lo único que hace es contestar al que lo pidió.
164
+ */
165
+ function hiloQueNoArranca(nombre, esperados, params, modelo, logger) {
166
+ const padre = params.parent;
167
+ const texto = `${nombre} NO HA ARRANCADO: espera a que termine ${esperados.join(", ")}, que sigue trabajando y produce lo que ${nombre} necesita. ` +
168
+ `Cuando ${esperados.join(", ")} te devuelva su informe, vuelve a llamar a ${nombre} con ese informe dentro del encargo.`;
169
+ return new AgentThread({
170
+ definition: { modelClient: modelo, iterationLimit: 1 },
171
+ threadId: params.threadId,
172
+ title: params.request.name,
173
+ parent: params.parent,
174
+ agentInfo: { type: "dynamic", ...params.request },
175
+ preComputedCompletion: {
176
+ type: "done",
177
+ output: { role: "assistant", content: texto },
178
+ send_to_parent: { role: "tool", tool_call_id: padre.tool_call_id ?? "", content: texto },
179
+ },
180
+ tracing: NOOP_AGENT_TRACING,
181
+ logger: logger,
182
+ });
183
+ }
184
+ /**
185
+ * Lo que se le cuenta a quien ESPERÓ a otros especialistas, al soltarse: a quién esperó y qué hay en el disco.
186
+ * Solo NOMBRES —los recursos de `icons/` y los `ASSETS.md`/`TASKS.md` del plan—, nunca su contenido: es lo que dejó
187
+ * el otro, que no viaja por ningún canal más que el disco. Sin recursos que nombrar, un aviso corto.
188
+ */
189
+ export function textoDeLoQueDejaronLosEsperados(raiz, esperadas) {
190
+ let recursos = [];
191
+ try {
192
+ recursos = readdirSync(join(raiz, "icons")).filter((f) => /\.(svg|png|jpg|jpeg)$/i.test(f)).sort();
193
+ }
194
+ catch {
195
+ recursos = [];
196
+ }
197
+ let notas = [];
198
+ try {
199
+ notas = readdirSync(join(raiz, ".xonecode", "planes"))
200
+ .flatMap((p) => ["ASSETS.md"].filter((f) => existsSync(join(raiz, ".xonecode", "planes", p, f))).map((f) => `/planes/${p}/${f}`))
201
+ .sort();
202
+ }
203
+ catch {
204
+ notas = [];
205
+ }
206
+ return [
207
+ `Antes de ti han terminado ${esperadas.join(", ")}, que trabajaban a la vez o antes que tú.`,
208
+ ...(recursos.length === 0 ? [] : [`Lo que hay ahora en icons/ (${String(recursos.length)}): ${recursos.join(", ")}.`]),
209
+ ...(notas.length === 0 ? [] : [`Lo que dejó escrito para ti: ${notas.join(", ")}. Léelo antes de escribir.`]),
210
+ "USA los recursos que te sirvan por su nombre exacto, y si no vas a usar alguno, dilo al devolver el trabajo.",
211
+ ].join("\n");
212
+ }
213
+ /**
214
+ * Lo que se le dice a quien puede llamar a otros. Cada llamada de estas es un hilo nuevo que solo sabe lo que le
215
+ * escribas (aunque recuerde sus encargos anteriores de la sesión), y lo que te devuelve es lo que vio, no un
216
+ * veredicto: decidir si lo que escribiste funciona sigue siendo cosa tuya, con lo que ese hijo te traiga.
217
+ */
218
+ export function textoDelBucle(llama, opciones = {}) {
219
+ const puedeDiseñar = llama.includes("designer-xone");
220
+ return [
221
+ `PUEDES LLAMAR A: ${llama.join(", ")}, con \`create_sub_agent\` (\`name\` exacto y \`input\` autosuficiente).`,
222
+ "Tu bucle es: escribe, llama al de pruebas para desplegar y comprobar, lee lo que te devuelve y corrige.",
223
+ "Dile QUÉ comprobar y qué esperas ver. No lo llames por cada línea que cambies: junta los cambios.",
224
+ "",
225
+ "PRUEBAS FUNCIONALES Y VISUALES, SEPARADAS Y EN ESTE ORDEN:",
226
+ "- Instrumenta tu código con `console.log(\"CALC tecla 8\")`, `console.log(\"CALC resultado \" + r)` en los puntos",
227
+ " que deciden (tecla, expresión, resultado, error). Nunca `ui.showToast` ni `appData.writeConsoleString` para depurar:",
228
+ " el aviso no deja rastro y el segundo es legacy y no llega al log de Android.",
229
+ "- FUNCIONAL: UN encargo con TODOS los casos, como acciones y lo que esperas («teclea 2 + 3 × 4 = → log `CALC resultado",
230
+ " 14`, display 14»). El de pruebas los hace, lee el log con `xone-log-android --app` y te devuelve cada caso: pasa o falla",
231
+ " con lo que salió. SIN capturas. Corrige todos los fallos juntos y repite solo los que fallaron.",
232
+ "- VISUAL, solo cuando lo funcional pasa: UNA captura por ronda. Con ella el harness te trae la medida contra la maqueta;",
233
+ " pasa el crítico, arregla TODAS las diferencias en la misma ronda y solo entonces pide otra captura. Tres rondas como mucho.",
234
+ "- Una pantalla que se parece a la maqueta con datos de ejemplo fijos NO está terminada: la lógica tiene que funcionar.",
235
+ "Lo que te devuelva es lo que ha visto, no un permiso para dar el trabajo por bueno: si dice que la app se cae, se cae.",
236
+ "EMPIEZA POR LO MÍNIMO: no escribas la pantalla entera de una vez. Primero la versión más pequeña que se pueda VER y PULSAR",
237
+ "—el arranque, el display y UN botón—, y pídele al de pruebas que la arranque y la toque. Solo cuando eso funcione, amplía",
238
+ "(el teclado, las funciones, el historial). Un fallo de arranque descubierto con la pantalla entera escrita cuesta reescribirla.",
239
+ ...(puedeDiseñar
240
+ ? [
241
+ "El LAYOUT y el CSS son TUYOS: si la pantalla se ve mal, lo arreglas tú, no se lo pasas a nadie ni lo devuelves al que te",
242
+ "encargó. `designer-xone` solo hace RECURSOS —iconos de una biblioteca y fondos SVG en `icons/`—: pídeselos juntos, con para",
243
+ "qué control, tamaño en píxeles y colores, y usa los nombres que te devuelva. No le pidas que toque el `.xne` ni el CSS: no puede.",
244
+ "Los recursos te llegan con una TABLA (fichero → control → atributo → tamaño → si lleva el símbolo dentro): úsala y no abras",
245
+ "los SVG para enterarte de lo que ya dice.",
246
+ ]
247
+ : []),
248
+ ...(opciones.conCritica === true
249
+ ? [
250
+ "",
251
+ "PARA JUZGAR UNA PANTALLA tienes `xone_critica_visual` (un modelo aparte mira la captura) y `comparar_capturas` (mide la",
252
+ "estructura contra la maqueta con números). Pídele al de pruebas una captura y que te diga su nombre; pásala con `pantalla` y,",
253
+ "si hay diseño (/diseno/ o un adjunto), como `referencia`. Lo que digan esas dos herramientas manda sobre tu impresión: no",
254
+ "lo descartes como «no fiable»; si no estás de acuerdo, dilo con el dato.",
255
+ "Cada vez que vuelva el de pruebas con una captura, el HARNESS te trae su medida contra la maqueta: no hace falta que",
256
+ "la pidas tú. El juicio es tuyo: con esa cifra y el crítico decides si sigues; no se lo devuelvas al que te encargó para que juzgue.",
257
+ ]
258
+ : []),
259
+ "",
260
+ "NO TERMINAS hasta que se cumpla el CRITERIO DE ACEPTACIÓN de tu encargo. Si el encargo no lo trae, es: (1) todas las casillas",
261
+ "de la tarea en el `TASKS.md` comprobadas, (2) la app arranca y hace lo que pide, y (3) con diseño, la distancia que mide",
262
+ "`comparar_capturas` por debajo del 10 % en vertical y en horizontal, que ENCAJE en la pantalla (cada borde del contenido a",
263
+ "un 3 % o menos del de la maqueta: que llegue a los lados y abajo como ella), y el crítico sin diferencias de forma ni de estructura.",
264
+ "Lo que XOne NO puede reproducir no cuenta y no lo persigas: las tipografías del diseño (no hay .ttf), el desenfoque de fondo y",
265
+ "las animaciones; una sombra o un degradado se aproximan con un SVG. Tienes TRES vueltas de corregir y volver a comprobar para",
266
+ "llegar; si al agotarlas algo",
267
+ "sigue sin cumplirse, devuelve el trabajo diciendo QUÉ falta, con lo que midieron el crítico y la medida tal cual. Devolver antes",
268
+ "porque «ya funciona» es dejar el trabajo a medias. Al devolver cuenta lo que se midió: el juicio final no es tuyo, y el arnés",
269
+ "lo revisa igual.",
270
+ ].join("\n");
271
+ }
272
+ export function notaDeDelegacion(agentes, opciones = {}) {
84
273
  if (agentes.length === 0)
85
274
  return "";
86
275
  return [
@@ -88,7 +277,38 @@ export function notaDeDelegacion(agentes) {
88
277
  "`name` es EXACTAMENTE el nombre del especialista y `input` el encargo, autosuficiente —el",
89
278
  "especialista no ve esta conversación—. Donde estas instrucciones dicen `task` o `subagent_type`,",
90
279
  "entiende `create_sub_agent` y `name`. Las fichas de los especialistas:",
91
- ...agentes.map((a) => `- ${a.nombre}: ${fichaDeAgente(a)}`),
280
+ ...agentes.map((a) => `- ${a.nombre}: ${fichaDeAgente(a, opciones)}`),
281
+ ...(opciones.conComparacion === true ? ["", BUCLE_DE_CALIDAD] : []),
282
+ ...(opciones.conEsperas === true
283
+ ? [
284
+ "",
285
+ "ESPERAS ENTRE ESPECIALISTAS: developer-xone NO arranca mientras designer-xone esté trabajando (lo que el diseñador produce",
286
+ "—los recursos de `icons/`— lo necesita el `.xne`). Si los lanzas a la vez, la llamada al desarrollador te vuelve AL INSTANTE",
287
+ "diciendo que no ha arrancado; cuando el diseñador te devuelva su informe, llama de nuevo al desarrollador con ESE INFORME dentro",
288
+ "del encargo: su TABLA de recursos (fichero → control → atributo → tamaño) TAL CUAL, no un resumen, y no revises tú los SVG. Lo más simple es lanzar primero al diseñador y, con su informe, al desarrollador.",
289
+ ]
290
+ : []),
291
+ ...(opciones.conBucle === true
292
+ ? [
293
+ "",
294
+ "BUCLE DEL DESARROLLADOR: developer-xone prueba lo que escribe él mismo, llamando a device-controller (despliega, toca y lee",
295
+ "el log), maqueta y arregla lo visual él mismo —designer-xone solo le hace iconos y fondos SVG—, y corrige hasta cumplir el CRITERIO DE ACEPTACIÓN que le des. Por eso",
296
+ "cada encargo a developer-xone lleva el CRITERIO ESCRITO y MEDIBLE: qué casillas del plan tiene que dejar comprobadas, qué",
297
+ "tiene que hacer la app, y, si hay diseño, que la distancia de `comparar_capturas` quede por debajo del 10 % en vertical y en",
298
+ "horizontal, que ENCAJE en la pantalla (cada borde a un 3 % o menos del de la maqueta) y que el crítico no señale diferencias de forma o de estructura (di dónde está la maqueta). No se puede pedir",
299
+ "lo que XOne no reproduce —las tipografías del diseño, el desenfoque de fondo, las animaciones—. Sin criterio, devolverá el",
300
+ "trabajo en cuanto funcione. No le encargues a device-controller comprobar SU",
301
+ "trabajo, ni a designer-xone lo visual de su pantalla (designer-xone no puede tocar un `.xne` ni el CSS): llama tú al diseñador",
302
+ "para los RECURSOS que la pantalla necesite, antes del desarrollador. Lo que developer-xone te devuelva es lo que él vio y midió, no un veredicto: el juicio final es tuyo y del arnés.",
303
+ ]
304
+ : []),
305
+ ...(opciones.conMemoria === true
306
+ ? [
307
+ "",
308
+ "MEMORIA DE LOS ESPECIALISTAS: en esta sesión cada especialista RECUERDA sus encargos anteriores (lo que leyó,",
309
+ "lo que descubrió y lo que hizo). Al volver a llamar a uno, dale solo lo NUEVO: no repitas lo que ya sabe.",
310
+ ]
311
+ : []),
92
312
  ].join("\n");
93
313
  }
94
314
  /** Lo que cada clase NO puede hacer, que la lista de tools no dice sola. */
@@ -101,6 +321,14 @@ const LIMITES_DE = {
101
321
  * La frase de las tools que TIENE, sacada de los nombres que de verdad se le montan: escrita a
102
322
  * mano se quedaba vieja en cuanto se añadía una —la nota decía cuatro y el modelo veía seis—.
103
323
  */
324
+ /**
325
+ * CUÁNDO usar el índice de atributos, junto a la lista de tools y solo a quien la tiene. Medido en calc13: con la tool
326
+ * montada y descrita, el consultor la consultó 9 veces y el desarrollador —quien escribe los atributos— ninguna, con 13
327
+ * lecturas de la skill: su prompt solo la nombraba en la lista, y el `SKILL.md` le manda a las referencias. No va en el
328
+ * `SKILL.md` porque ese llega también a los motores externos, que no tienen la tool.
329
+ */
330
+ export const USO_DE_XONE_ATRIBUTOS = "Para saber si un atributo XML existe en un nodo (coll, group, frame, prop) y qué hace, pregunta PRIMERO a `xone_atributos`: " +
331
+ "contesta en una línea desde las tablas de la skill. Lee la referencia de la skill solo si no lo encuentra, o si necesitas ejemplos o matices.";
104
332
  export function notaDeTools(nombres, limite) {
105
333
  return `NOTA DEL HARNESS: aunque tu identidad diga lo contrario, NO tienes las mismas tools que quien te delega. Tienes ${nombres
106
334
  .map((n) => `\`${n}\``)
@@ -175,7 +403,9 @@ export async function abrirSesionTrueforge(opciones) {
175
403
  * parámetro solo sirve para doblarla—, que es lo que evita dejarla escrita y sin montar.
176
404
  */
177
405
  const entornoDeDiagnostico = entornoConDepuracion(opciones.depurar === true);
178
- const diagnostico = opciones.diagnostico ?? crearDiagnosticoDeTools(raiz, entornoDeDiagnostico);
406
+ // El CHAT es la sesión con la que se abrió (`opciones.hilo`, el id del índice), no el `hilo` de
407
+ // ahora: `/nuevo` abre otro hilo huérfano pero los actos siguen yendo a la misma sesión.
408
+ const diagnostico = opciones.diagnostico ?? crearDiagnosticoDeTools(raiz, entornoDeDiagnostico, opciones.hilo);
179
409
  /**
180
410
  * La traza de EXCEPCIONES e HITOS, la MISMA de deepagents (`turnoReal.ts`) y que aquí
181
411
  * faltaba por completo: TrueForge nunca la encendía, ni siquiera con la variable de entorno
@@ -201,6 +431,36 @@ export async function abrirSesionTrueforge(opciones) {
201
431
  * viaja por el cable como nombre de un especialista tiene que serlo de verdad.
202
432
  */
203
433
  const especialistaDeHilo = new Map();
434
+ const conMemoriaDeEspecialistas = opciones.memoriaDeEspecialistas ?? process.env.XONECODE_SIN_MEMORIA_DE_ESPECIALISTAS !== "1";
435
+ // Encendido por omisión desde que se midió (calc9-calc15); `XONECODE_BUCLE_DEL_DEVELOPER=0` lo apaga para comparar.
436
+ const conBucleDelDesarrollador = opciones.bucleDelDesarrollador ?? process.env.XONECODE_BUCLE_DEL_DEVELOPER !== "0";
437
+ const conEsperas = opciones.esperasEntreHijos ?? true;
438
+ /** Cuántas comprobaciones (llamadas a quien EJECUTA) lleva cada hilo que lleva un lazo. */
439
+ const vueltasDeCadaHilo = new Map();
440
+ /** Cuántas CAPTURAS ha sacado cada hilo que ejecuta comandos (`xone-captura-android`, `xone-hotswap shot`). */
441
+ const capturasDeCadaHilo = new Map();
442
+ /** Lo que escribió cada hijo en su encargo, quién lo llamó, y los informes que esperan a ese padre (`informesDeHijos.ts`). */
443
+ const escritosDeCadaHilo = new Map();
444
+ const padreDeHilo = new Map();
445
+ const informesPendientes = new Map();
446
+ /** Las escrituras pedidas y aún sin respuesta, por hilo e id: la respuesta de una aprobada llega en OTRO `paso`. */
447
+ const escriturasEnVuelo = new Map();
448
+ /** Para la medida automática (`medidaAutomatica.ts`): cuándo nació cada hilo, su última escritura, su última crítica y la última medida que recibió. */
449
+ const nacimientoDeHilo = new Map();
450
+ const ultimaEscrituraDeHilo = new Map();
451
+ const ultimaCriticaDeHilo = new Map();
452
+ const ultimaMedidaDeHilo = new Map();
453
+ /** Cuántas rondas VISUALES lleva cada hilo: una por medida automática recibida (`RONDAS_VISUALES`). */
454
+ const rondasVisualesDeHilo = new Map();
455
+ const capacidadDeInformes = capacidadDeInformesDeHijos((hiloPadre) => {
456
+ const textos = informesPendientes.get(hiloPadre) ?? [];
457
+ informesPendientes.delete(hiloPadre);
458
+ return textos;
459
+ });
460
+ const esperas = crearEsperas();
461
+ const memoriaDeEspecialistas = crearMemoriaDeEspecialistas();
462
+ /** Los hijos con memoria que corren ahora: su hilo, para leer su historial al terminar. */
463
+ const hijosConMemoria = new Map();
204
464
  /** Lo que el agente dejó en `/artefactos/` y aún no se ha anunciado: un artefacto se escribe
205
465
  * SIN aprobación, así que tiene que ANUNCIARSE, como en deepagents. */
206
466
  const artefactosPorAnunciar = [];
@@ -302,21 +562,36 @@ export async function abrirSesionTrueforge(opciones) {
302
562
  const cargarEstilos = estilosDeDisco(raiz);
303
563
  const navegacion = () => crearNavegacionXone(cargarIndice, ficheros, cargarEstilos);
304
564
  const carpeta = opciones.artefactos;
565
+ const lectorDeReferencias = crearLectorDeReferencias({
566
+ raiz,
567
+ ...(carpeta === undefined ? {} : { artefactos: carpeta }),
568
+ ...(opciones.adjuntos === undefined ? {} : { adjuntos: opciones.adjuntos }),
569
+ });
570
+ /** El crítico y la medida, con las MISMAS piezas para quien las tenga: el raíz, y con el bucle encendido el desarrollador. */
571
+ const herramientasDeJuicio = () => carpeta === undefined
572
+ ? []
573
+ : [
574
+ crearCriticaVisual({
575
+ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)),
576
+ invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
577
+ // La maqueta casi nunca está en /artefactos/: la trae la persona (/adjuntos/) o vive en /diseno/.
578
+ leerReferencia: lectorDeReferencias,
579
+ }),
580
+ // Medir la estructura contra la maqueta: la otra mitad del crítico, con el raíz como él.
581
+ crearCompararCapturas({ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)), leerReferencia: lectorDeReferencias }),
582
+ ];
583
+ // ¿Existe este atributo en este nodo? A todos, como la navegación: es de lectura y contesta desde la skill del paquete.
584
+ const atributos = crearAtributosXone();
305
585
  const propiasDelRaiz = [
306
586
  navegacion(),
307
- ...(carpeta === undefined
308
- ? []
309
- : [
310
- crearCriticaVisual({
311
- leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)),
312
- invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
313
- }),
314
- crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto }),
315
- ]),
587
+ atributos,
588
+ ...herramientasDeJuicio(),
589
+ ...(carpeta === undefined ? [] : [crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto })]),
316
590
  ];
317
591
  const propiasDe = (agente) => [
318
592
  crearBusquedaRegex(backend),
319
593
  navegacion(),
594
+ atributos,
320
595
  ...(carpeta !== undefined && (agente.escribeEn ?? []).length > 0
321
596
  ? [crearCopiarArtefacto({ raiz, carpetaDeArtefactos: carpeta, perfil: agente })]
322
597
  : []),
@@ -328,8 +603,24 @@ export async function abrirSesionTrueforge(opciones) {
328
603
  ...(opciones.adjuntos !== undefined && recibeIncorporarAdjunto(agente)
329
604
  ? [crearIncorporarAdjunto({ raiz, carpetaDeAdjuntos: opciones.adjuntos, perfil: agente })]
330
605
  : []),
606
+ // Buscar un icono cuando faltan los assets (IXCODE-18), con el reparto de deepagents: a quien
607
+ // escribe el proyecto y solo con el puerto. Es de LECTURA: no entra en `requireApprovalForTools`.
608
+ ...(opciones.iconos !== undefined && recibeBuscarIcono(agente)
609
+ ? [crearBuscarIcono(opciones.iconos)]
610
+ : []),
611
+ // Fondos SVG: pura y sin red, sin puerto; a quien escribe el proyecto, como deepagents.
612
+ ...(recibeGenerarFondo(agente) ? [crearGenerarFondoSvg()] : []),
613
+ // ¿Cambió la zona tras el toque? Al conductor, que es quien saca las capturas: comprueba una
614
+ // ACCIÓN suya, no juzga la pantalla (eso sigue siendo del crítico y de `comparar_capturas`).
615
+ ...(carpeta !== undefined && agente.ejecucion === true
616
+ ? [crearDiferenciaDeCapturas({ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)) })]
617
+ : []),
331
618
  // Y marcar en el plan lo comprobado en el aparato, con el reparto de deepagents: a quien ejecuta.
332
619
  ...(agente.ejecucion === true ? [crearMarcarCriteriosDelPlan({ raiz })] : []),
620
+ // El BUCLE del desarrollador: quien puede llamar al de pruebas también tiene el crítico y la medida, para
621
+ // juzgar la pantalla con lo que el otro capture. El veredicto sigue siendo de un modelo aparte, no suyo, y el
622
+ // juez final del arnés revisa igual (`textoDelBucle`).
623
+ ...(conBucleDelDesarrollador && (agente.llama?.length ?? 0) > 0 ? herramientasDeJuicio() : []),
333
624
  ];
334
625
  /**
335
626
  * **Un cliente de modelo por papel, modelo y esfuerzo, que dura la SESIÓN** —hasta `/modelo`—, y
@@ -353,7 +644,13 @@ export async function abrirSesionTrueforge(opciones) {
353
644
  }
354
645
  return cliente;
355
646
  };
356
- const llm = modeloParaTrueforge({ modelo: () => clienteDe("papel:trabajo:", () => modelos.paraPapel("trabajo")), senal: () => aborto?.signal });
647
+ // Lo que la librería pierde: el razonamiento de cada llamada, por tokens de salida, hasta que su evento llega.
648
+ const razonamientoPendiente = new Map();
649
+ const alRazonar = (salida, razonamiento) => {
650
+ razonamientoPendiente.set(salida, [...(razonamientoPendiente.get(salida) ?? []), razonamiento]);
651
+ };
652
+ const razonamientoDeSalida = (salida) => razonamientoPendiente.get(salida)?.shift();
653
+ const llm = modeloParaTrueforge({ modelo: () => clienteDe("papel:trabajo:", () => modelos.paraPapel("trabajo")), senal: () => aborto?.signal, alRazonar });
357
654
  /**
358
655
  * El hilo de un hijo EXTERNO: un `AgentThread` normal con UNA llamada, cuyo «modelo» es el
359
656
  * producto (`modeloExterno.ts`). Sin tools, sin capabilities y sin compactación: el bucle vive
@@ -400,8 +697,37 @@ export async function abrirSesionTrueforge(opciones) {
400
697
  * LECTURA y sin shell: un nombre inventado no tumba el turno ni se lleva la escritura.
401
698
  */
402
699
  const crearHijo = async (params) => {
403
- const agente = especialistas().find((a) => a.nombre === params.request.name);
700
+ // Quién lo pide: el raíz, o un especialista que llama a otro. Un especialista solo puede llamar a los que su
701
+ // `.md` declara en `llama` y con el interruptor puesto; lo demás cae al ayudante genérico de solo lectura,
702
+ // el mismo de un nombre inventado, y se anota en la traza.
703
+ const quienPide = params.parent === undefined ? undefined : especialistaDeHilo.get(params.parent.thread_id ?? "");
704
+ const permitidos = quienPide === undefined ? undefined : (especialistas().find((a) => a.nombre === quienPide)?.llama ?? []);
705
+ const concedido = permitidos === undefined || (conBucleDelDesarrollador && permitidos.includes(params.request.name));
706
+ if (!concedido)
707
+ anotarPaso("trueforge.llamada", `${quienPide ?? "?"} pidió a ${params.request.name}: no está en su lista`)();
708
+ const agente = concedido ? especialistas().find((a) => a.nombre === params.request.name) : undefined;
709
+ // Cuenta una COMPROBACIÓN: quien lleva un lazo llamó a quien ejecuta. Es lo que dispara el aviso de vueltas.
710
+ if (concedido && agente?.ejecucion === true && params.parent !== undefined) {
711
+ const padre = params.parent.thread_id ?? "";
712
+ if (padre !== "")
713
+ vueltasDeCadaHilo.set(padre, (vueltasDeCadaHilo.get(padre) ?? 0) + 1);
714
+ }
715
+ // ¿Arranca? Un especialista que declara `espera` NO arranca mientras haya vivo —o anunciado por el mismo mensaje del
716
+ // orquestador— uno de esos: se le devuelve al instante diciéndolo, y el orquestador lo llama de nuevo con el informe del
717
+ // otro. No se espera bloqueando: bloqueaba a la librería, que no devuelve el control mientras un hilo no termine su
718
+ // paso, y una aprobación del otro hilo no se atendía (`esperas.ts`).
719
+ const debeEsperar = conEsperas && agente !== undefined ? esperas.vivos(agente.espera ?? [], params.threadId) : [];
720
+ if (agente !== undefined && debeEsperar.length > 0) {
721
+ anotarPaso("trueforge.espera", `${agente.nombre} no arranca: espera a ${debeEsperar.join(", ")}`)();
722
+ diagnostico?.memoria?.(agente.nombre, "arranca-sin", `espera a ${debeEsperar.join(", ")}`);
723
+ return hiloQueNoArranca(agente.nombre, debeEsperar, params, llm, logger);
724
+ }
404
725
  detencion.nacio(params.threadId);
726
+ esperas.nacio(agente?.nombre ?? params.request.name, params.threadId);
727
+ const hiloDelPadre = params.parent?.thread_id;
728
+ if (hiloDelPadre !== undefined && hiloDelPadre !== "")
729
+ padreDeHilo.set(params.threadId, hiloDelPadre);
730
+ nacimientoDeHilo.set(params.threadId, Date.now());
405
731
  quienEs.set(params.threadId, agente?.nombre ?? params.request.name);
406
732
  if (agente !== undefined)
407
733
  especialistaDeHilo.set(params.threadId, agente.nombre);
@@ -413,6 +739,13 @@ export async function abrirSesionTrueforge(opciones) {
413
739
  backend: backend,
414
740
  propias: propiasDe,
415
741
  notas: capacidadDeNotasDeLaSesion,
742
+ puedeLlamar: (a) => conBucleDelDesarrollador && (a.llama?.length ?? 0) > 0,
743
+ informes: capacidadDeInformes,
744
+ vueltas: (a) => a.ejecucion === true
745
+ ? capacidadDeAvisoDeVueltas({ vueltasDe: (hilo) => capturasDeCadaHilo.get(hilo) ?? 0, umbrales: UMBRALES_DE_CAPTURAS, texto: textoDeCapturas })
746
+ : conBucleDelDesarrollador && (a.llama?.length ?? 0) > 0
747
+ ? capacidadDeAvisoDeVueltas({ vueltasDe: (hilo) => vueltasDeCadaHilo.get(hilo) ?? 0, umbrales: UMBRALES_DE_VUELTAS, texto: textoDeVueltas })
748
+ : undefined,
416
749
  conShell: () => montarBackend({
417
750
  entorno: entornoDeLaShellDelProyecto(raiz, opciones.artefactos),
418
751
  // El MISMO patrón que ya usa `modeloParaTrueforge` un poco más arriba en este
@@ -422,20 +755,56 @@ export async function abrirSesionTrueforge(opciones) {
422
755
  }),
423
756
  });
424
757
  const nombres = toolsDe(piezas);
425
- const nota = notaDeTools(nombres, agente === undefined ? LIMITE_DEL_GENERICO : LIMITES_DE[clase]);
758
+ const nota = [
759
+ notaDeTools(nombres, agente === undefined ? LIMITE_DEL_GENERICO : LIMITES_DE[clase]),
760
+ ...(nombres.includes(NOMBRE_ATRIBUTOS_XONE) ? [USO_DE_XONE_ATRIBUTOS] : []),
761
+ ].join("\n");
426
762
  const instrucciones = agente === undefined
427
- ? `${nota} Contesta con lo que encuentres y dónde.`
428
- : [promptDeAgente(agente, repartirSkills(agente, disponibles)), "", nota, anuncioDeSkills(agente, catalogo)]
763
+ ? `${IDIOMA_DE_LA_RESPUESTA}\n\n${nota} Contesta con lo que encuentres y dónde.`
764
+ : [
765
+ IDIOMA_DE_LA_RESPUESTA,
766
+ "",
767
+ promptDeAgente(agente, repartirSkills(agente, disponibles)),
768
+ "",
769
+ nota,
770
+ anuncioDeSkills(agente, catalogo),
771
+ ...(conBucleDelDesarrollador && (agente.llama?.length ?? 0) > 0 ? ["", textoDelBucle(agente.llama ?? [], { conCritica: carpeta !== undefined })] : []),
772
+ ]
429
773
  .filter((l) => l !== undefined)
430
774
  .join("\n")
431
775
  .trimEnd();
432
776
  const papel = agente?.soloLectura === true ? "rapido" : "trabajo";
777
+ const claseDeEsfuerzo = agente === undefined ? undefined : claseDeTrabajo(agente);
778
+ // La memoria del especialista: su conversación anterior de ESTA sesión, si la hay y se puede usar.
779
+ const apertura = conMemoriaDeEspecialistas && agente !== undefined ? memoriaDeEspecialistas.abrir(agente.nombre, params.threadId) : undefined;
780
+ const previa = apertura !== undefined && "previa" in apertura ? apertura.previa : undefined;
781
+ if (agente !== undefined && apertura !== undefined) {
782
+ if (previa !== undefined)
783
+ diagnostico?.memoria?.(agente.nombre, "arranca-con", "historial de la sesión", previa.tokens);
784
+ else
785
+ diagnostico?.memoria?.(agente.nombre, "arranca-sin", "sin" in apertura ? apertura.sin : "primera");
786
+ }
787
+ // Si lo que esperaba ya terminó en esta conversación, sabe qué dejó (solo nombres, nunca contenido).
788
+ const esperado = conEsperas && agente !== undefined ? esperas.terminados(agente.espera ?? []) : [];
789
+ const avisoDeLoEsperado = esperado.length > 0 ? `${textoDeLoQueDejaronLosEsperados(raiz, esperado)}\n\n` : "";
790
+ const mensajesIniciales = previa === undefined
791
+ ? [{ role: "user", content: `${avisoDeLoEsperado}${params.request.input}` }]
792
+ : [
793
+ ...previa.mensajes,
794
+ {
795
+ role: "user",
796
+ content: "NUEVO ENCARGO. Ya trabajaste antes en esta sesión: lo de arriba es tu conversación anterior, con lo que leíste y decidiste. " +
797
+ "No repitas lecturas de ficheros que no hayan cambiado.\n\n" +
798
+ `${avisoDeLoEsperado}${params.request.input}`,
799
+ },
800
+ ];
433
801
  const definicionDelHijo = {
434
802
  modelClient: modeloParaTrueforge({
435
803
  modelo: () => agente?.modelo === undefined
436
- ? clienteDe(`papel:${papel}:${agente?.esfuerzo ?? ""}`, () => modelos.paraPapel(papel, agente?.esfuerzo))
437
- : clienteDe(`modelo:${agente.modelo}:${agente.esfuerzo ?? ""}`, () => modelos.paraModelo(agente.modelo, agente.esfuerzo)),
804
+ ? clienteDe(`papel:${papel}:${agente?.esfuerzo ?? ""}:${claseDeEsfuerzo ?? ""}:${agente?.pensamiento ?? ""}`, () => modelos.paraPapel(papel, agente?.esfuerzo, claseDeEsfuerzo, agente?.pensamiento))
805
+ : clienteDe(`modelo:${agente.modelo}:${agente.esfuerzo ?? ""}:${claseDeEsfuerzo ?? ""}:${agente.pensamiento ?? ""}`, () => modelos.paraModelo(agente.modelo, agente.esfuerzo, claseDeEsfuerzo, agente.pensamiento)),
438
806
  senal: () => aborto?.signal,
807
+ alRazonar,
439
808
  soloTexto: () => (detencion.soloTexto(params.threadId) ? RESUMEN_DE_RELLENO : undefined),
440
809
  corte: () => detencion.corte(params.threadId),
441
810
  alTirarLlamadas: (n) => {
@@ -444,11 +813,11 @@ export async function abrirSesionTrueforge(opciones) {
444
813
  anotarPaso("trueforge.detener", `${quienEs.get(params.threadId) ?? params.threadId}: ${n} tool call(s) tiradas`)();
445
814
  },
446
815
  }),
447
- messages: [{ role: "user", content: params.request.input }],
816
+ messages: mensajesIniciales,
448
817
  // Los topes MEDIDOS de deepagents: el conductor más, porque cada paso suyo es un comando.
449
818
  iterationLimit: agente?.ejecucion === true ? TOPE_DE_LLAMADAS_DEL_CONDUCTOR : TOPE_DE_LLAMADAS_DEL_ESPECIALISTA,
450
819
  };
451
- return new AgentThread({
820
+ const hijo = new AgentThread({
452
821
  definition: definicionDelHijo,
453
822
  threadId: params.threadId,
454
823
  title: params.request.name,
@@ -473,6 +842,9 @@ export async function abrirSesionTrueforge(opciones) {
473
842
  tracing: NOOP_AGENT_TRACING,
474
843
  logger,
475
844
  });
845
+ if (agente !== undefined && apertura !== undefined)
846
+ hijosConMemoria.set(params.threadId, { nombre: agente.nombre, hilo: hijo, iniciales: mensajesIniciales });
847
+ return hijo;
476
848
  };
477
849
  /**
478
850
  * El árbol de hilos de UNA conversación. Se rehace con `nuevoHilo`, al reabrir una sesión
@@ -484,9 +856,13 @@ export async function abrirSesionTrueforge(opciones) {
484
856
  /** La pregunta del orquestador que espera respuesta: el siguiente mensaje la contesta. */
485
857
  let preguntaEnEspera;
486
858
  const nuevoOrquestador = (foto) => {
859
+ // Los hijos que corrían no pueden seguir; lo que aprendieron sí se queda.
860
+ memoriaDeEspecialistas.darPorMuertos();
861
+ esperas.darPorMuertos();
862
+ hijosConMemoria.clear();
487
863
  const definicion = {
488
864
  modelClient: llm,
489
- instruction: [promptOrquestador(especialistas()), notaDeDelegacion(especialistas())].filter((l) => l !== "").join("\n\n"),
865
+ instruction: [IDIOMA_DE_LA_RESPUESTA, promptOrquestador(especialistas()), notaDeDelegacion(especialistas(), { conIconos: opciones.iconos !== undefined, conComparacion: carpeta !== undefined, conMemoria: conMemoriaDeEspecialistas, conBucle: conBucleDelDesarrollador, conEsperas })].filter((l) => l !== "").join("\n\n"),
490
866
  // Por TURNO, porque el raíz se rehace desde su foto al final de cada uno (ver `turno`).
491
867
  iterationLimit: LIMITE_DE_LLAMADAS_DEL_RAIZ,
492
868
  };
@@ -503,6 +879,7 @@ export async function abrirSesionTrueforge(opciones) {
503
879
  capacidadDeRecortes(backend),
504
880
  capacidadDeFecha(),
505
881
  capacidadDeNotasDeLaSesion,
882
+ capacidadDeInformes,
506
883
  ]),
507
884
  /**
508
885
  * **La conversación se RESUME al mismo umbral que deepagents** (`UMBRAL_RESUMEN_TOKENS`):
@@ -511,7 +888,10 @@ export async function abrirSesionTrueforge(opciones) {
511
888
  * instrucciones van en el prompt de sistema —aquí `instruction`, en un hijo su
512
889
  * capability—, que no se compacta.
513
890
  */
514
- contextCompaction({ definition: definicion, compactionThresholdTokens: UMBRAL_RESUMEN_TOKENS }),
891
+ contextCompaction({
892
+ definition: { ...definicion, modelClient: conResumenSeguro(definicion.modelClient, (t) => anotarPaso("trueforge.compactacion", t)()) },
893
+ compactionThresholdTokens: UMBRAL_RESUMEN_TOKENS,
894
+ }),
515
895
  // `ask_user_question`, SOLO en el raíz —la librería tampoco se la da a un hijo—: es el
516
896
  // único que tiene a una persona delante.
517
897
  askUserQuestion(),
@@ -572,6 +952,39 @@ export async function abrirSesionTrueforge(opciones) {
572
952
  * `thread_id`: mandarla al raíz no la contestaría —o la rechazaría por hilo desconocido—.
573
953
  * Por eso las tool calls se apuntan por hilo Y por id: dos hijos pueden repetir id.
574
954
  */
955
+ /**
956
+ * Cuando vuelve quien EJECUTA, la medida de su última captura contra la maqueta, para quien lo llamó
957
+ * (`medidaAutomatica.ts`). Sin carpeta de artefactos, sin maqueta o sin captura nueva, nada. Un fallo al abrir o
958
+ * decodificar se anota y no tumba el turno.
959
+ */
960
+ function medidaTrasProbar(deHilo, hiloPadre, quien) {
961
+ if (carpeta === undefined || quien?.ejecucion !== true)
962
+ return undefined;
963
+ const maqueta = buscarMaqueta(raiz, opciones.adjuntos);
964
+ const captura = ultimaCaptura(carpeta, nacimientoDeHilo.get(deHilo) ?? Date.now());
965
+ if (maqueta === undefined || captura === undefined)
966
+ return undefined;
967
+ try {
968
+ const comparacion = medirContraMaqueta(join(carpeta, captura), maqueta.disco);
969
+ ultimaMedidaDeHilo.set(hiloPadre, resumenDeMedida(comparacion, captura));
970
+ const ronda = (rondasVisualesDeHilo.get(hiloPadre) ?? 0) + 1;
971
+ rondasVisualesDeHilo.set(hiloPadre, ronda);
972
+ const escribio = ultimaEscrituraDeHilo.get(hiloPadre);
973
+ const critico = ultimaCriticaDeHilo.get(hiloPadre);
974
+ return textoDeMedidaAutomatica({
975
+ quien: quien.nombre,
976
+ captura,
977
+ maqueta: maqueta.virtual,
978
+ comparacion,
979
+ pedirCritica: critico === undefined || (escribio !== undefined && escribio > critico),
980
+ ronda,
981
+ });
982
+ }
983
+ catch (e) {
984
+ anotarPaso("trueforge.medida", `no se pudo medir ${captura}: ${e instanceof Error ? e.message : String(e)}`)();
985
+ return undefined;
986
+ }
987
+ }
575
988
  async function* paso(lote, senal, pendientes, preguntas = []) {
576
989
  for await (const _ of orquestador.send(lote))
577
990
  void _;
@@ -594,9 +1007,23 @@ export async function abrirSesionTrueforge(opciones) {
594
1007
  args = {};
595
1008
  }
596
1009
  llamadas.set(claveDe(deHilo, t.id), { nombre: t.function.name, args });
1010
+ if (t.function.name === "write_file" || t.function.name === "edit_file")
1011
+ escriturasEnVuelo.set(claveDe(deHilo, t.id), { nombre: t.function.name, args });
1012
+ if (t.function.name === "xone_critica_visual")
1013
+ ultimaCriticaDeHilo.set(deHilo, Date.now());
597
1014
  // La traza: la MISMA lista blanca que el evento (`detalleDe`, `parametrosDe`), nunca
598
1015
  // los argumentos crudos.
599
- diagnostico?.herramienta(t.function.name, detalleDe(t.function.name, args), parametrosDe(t.function.name, args), tracker, deHilo === HILO_RAIZ ? "orquestador" : "especialista");
1016
+ const quien = quienEs.get(deHilo) ?? deHilo;
1017
+ diagnostico?.herramienta(t.function.name, detalleDe(t.function.name, args), parametrosDe(t.function.name, args), tracker, deHilo === HILO_RAIZ ? "orquestador" : "especialista", undefined, quien);
1018
+ // Una CAPTURA más de quien ejecuta: es lo que dispara el aviso de capturas (`textoDeCapturas`).
1019
+ if (t.function.name === "execute" && typeof args["command"] === "string" && COMANDO_DE_CAPTURA.test(args["command"])) {
1020
+ capturasDeCadaHilo.set(deHilo, (capturasDeCadaHilo.get(deHilo) ?? 0) + 1);
1021
+ }
1022
+ // Qué le pide el orquestador a quién: el encargo, acotado (ver `DiagnosticoDeTools.delegacion`).
1023
+ if (t.function.name === "create_sub_agent" && typeof args["name"] === "string" && typeof args["input"] === "string") {
1024
+ diagnostico?.delegacion?.(quien, args["name"], args["input"]);
1025
+ esperas.anunciar(args["name"]);
1026
+ }
600
1027
  }
601
1028
  }
602
1029
  }
@@ -605,8 +1032,52 @@ export async function abrirSesionTrueforge(opciones) {
605
1032
  const llamada = llamadas.get(claveDe(deHilo, evento.tool_call_id));
606
1033
  const chars = typeof evento.content === "string" ? evento.content.length : 0;
607
1034
  diagnostico?.resultado?.(llamada?.nombre, llamada === undefined ? undefined : detalleDe(llamada.nombre, llamada.args), chars);
1035
+ // Lo que ESCRIBIÓ este hilo, para decírselo a quien lo llamó al terminar (`informesDeHijos.ts`).
1036
+ const escritura = escriturasEnVuelo.get(claveDe(deHilo, evento.tool_call_id));
1037
+ if (escritura !== undefined) {
1038
+ escriturasEnVuelo.delete(claveDe(deHilo, evento.tool_call_id));
1039
+ if (deHilo !== HILO_RAIZ && escrituraConExito(evento.content)) {
1040
+ const cambios = escritosDeCadaHilo.get(deHilo) ?? new Map();
1041
+ anotarEscritura(cambios, escritura.nombre, escritura.args);
1042
+ escritosDeCadaHilo.set(deHilo, cambios);
1043
+ ultimaEscrituraDeHilo.set(deHilo, Date.now());
1044
+ }
1045
+ }
608
1046
  }
609
1047
  // Un hilo que agotó su tope se ANOTA en la traza, con quién era: el raíz y cualquier hijo.
1048
+ if (evento.type === "internal.agent.done") {
1049
+ esperas.murio(deHilo);
1050
+ const hiloPadre = padreDeHilo.get(deHilo);
1051
+ // Lo que devolvió a quien lo llamó, a la traza (`DiagnosticoDeTools.devolucion`): la otra mitad del encargo.
1052
+ const devuelto = evento.send_to_parent?.content;
1053
+ if (hiloPadre !== undefined && devuelto !== undefined) {
1054
+ const texto = typeof devuelto === "string" ? devuelto : JSON.stringify(devuelto);
1055
+ diagnostico?.devolucion?.(quienEs.get(deHilo) ?? deHilo, quienEs.get(hiloPadre) ?? (hiloPadre === HILO_RAIZ ? "orquestador" : hiloPadre), texto);
1056
+ }
1057
+ if (hiloPadre !== undefined) {
1058
+ padreDeHilo.delete(deHilo);
1059
+ const nombre = especialistaDeHilo.get(deHilo);
1060
+ const quien = especialistas().find((a) => a.nombre === nombre);
1061
+ const medida = medidaTrasProbar(deHilo, hiloPadre, quien);
1062
+ const texto = medida ??
1063
+ textoDelInforme(quienEs.get(deHilo) ?? deHilo, escritosDeCadaHilo.get(deHilo) ?? new Map(), quien !== undefined && quien.motor === "modelo" && clasesDeTools(quien) === "escribe", ultimaMedidaDeHilo.get(deHilo));
1064
+ escritosDeCadaHilo.delete(deHilo);
1065
+ nacimientoDeHilo.delete(deHilo);
1066
+ if (texto !== undefined)
1067
+ informesPendientes.set(hiloPadre, [...(informesPendientes.get(hiloPadre) ?? []), texto]);
1068
+ }
1069
+ const h = hijosConMemoria.get(deHilo);
1070
+ if (h !== undefined) {
1071
+ hijosConMemoria.delete(deHilo);
1072
+ const foto = h.hilo.toSnapshot();
1073
+ const tokens = (foto.current_context_usage?.prompt_tokens ?? 0) + (foto.current_context_usage?.completion_tokens ?? 0);
1074
+ const destino = memoriaDeEspecialistas.cerrar(deHilo, {
1075
+ // sin que nadie se lo haya pedido, así que no deja memoria (como uno que falló).
1076
+ bien: evento.status !== "error" && !detencion.soloTexto(deHilo), mensajes: [...h.iniciales, ...foto.context], tokens
1077
+ });
1078
+ diagnostico?.memoria?.(h.nombre, "cierra", destino, tokens);
1079
+ }
1080
+ }
610
1081
  const tope = topeAgotadoDe(evento);
611
1082
  if (tope !== undefined)
612
1083
  diagnostico?.corte?.(quienEs.get(deHilo) ?? deHilo, tope);
@@ -625,12 +1096,14 @@ export async function abrirSesionTrueforge(opciones) {
625
1096
  if (deHilo === HILO_RAIZ && evento.type === "internal.agent.context.append")
626
1097
  tracker.contexto = uso.input;
627
1098
  // Por ORIGEN, como deepagents: el orquestador y cada especialista por su nombre.
1099
+ const razonado = uso.razonamiento ?? razonamientoDeSalida(uso.output);
628
1100
  diagnostico?.modelo(quienEs.get(deHilo) ?? deHilo, {
629
1101
  input: uso.input,
630
1102
  output: uso.output,
631
1103
  cache: uso.cache,
632
1104
  llamadas: tracker.calls,
633
1105
  contexto: uso.input,
1106
+ ...(razonado === undefined ? {} : { razonamiento: razonado }),
634
1107
  });
635
1108
  avisar();
636
1109
  }
@@ -684,6 +1157,7 @@ export async function abrirSesionTrueforge(opciones) {
684
1157
  const memoriaDescartada = avisoDeMemoria;
685
1158
  avisoDeMemoria = undefined;
686
1159
  let cortadoPorTope = false;
1160
+ let preguntasSolas = 0;
687
1161
  let sinResolver = 0;
688
1162
  // El veredicto del turno, con las MISMAS reglas que deepagents (`verificacion.ts`).
689
1163
  let veredicto = "no-corrio";
@@ -709,6 +1183,36 @@ export async function abrirSesionTrueforge(opciones) {
709
1183
  const preguntas = [];
710
1184
  yield* paso(lote, senal, pendientes, preguntas);
711
1185
  if (pendientes.length === 0) {
1186
+ /**
1187
+ * **En modo autónomo no se le pregunta a nadie**, y se dice en el chat. Una pregunta con una opción
1188
+ * `(Recommended)` se contesta con ella; una sin recomendada vuelve al agente pidiéndole que decida él y diga
1189
+ * qué eligió. Era «todas o ninguna»: medido en MyAllXOne, el orquestador hizo DOS a la vez —una con
1190
+ * recomendada, la otra no— y no se contestó ninguna, se enseñó solo la primera y la segunda se perdió. Con
1191
+ * tope, para que no dé vueltas.
1192
+ */
1193
+ if (preguntas.length > 0 && opciones.sinAprobacion?.() === true && preguntasSolas + preguntas.length <= TOPE_DE_PREGUNTAS_CONTESTADAS_SOLAS) {
1194
+ preguntasSolas += preguntas.length;
1195
+ const respuestas = [];
1196
+ for (const q of preguntas) {
1197
+ const { pregunta, opciones: posibles } = consultaDe(q.args);
1198
+ const elegida = opcionRecomendada(posibles);
1199
+ yield {
1200
+ tipo: "aviso",
1201
+ texto: elegida !== undefined
1202
+ ? `modo autónomo: a «${pregunta}» contesto la recomendada: ${elegida}`
1203
+ : `modo autónomo: «${pregunta}» no se pregunta; lo decide el agente y lo dirá`,
1204
+ severidad: "aviso",
1205
+ };
1206
+ respuestas.push({
1207
+ type: "user.tool_response",
1208
+ thread_id: q.hilo,
1209
+ tool_call_id: q.id,
1210
+ content: elegida ?? RESPUESTA_AUTONOMA_SIN_RECOMENDADA,
1211
+ });
1212
+ }
1213
+ lote = respuestas;
1214
+ continue;
1215
+ }
712
1216
  // El orquestador PREGUNTA: el turno acaba aquí con la pregunta a la vista, y lo que la
713
1217
  // persona escriba después vuelve como la respuesta de esa tool (`turno`, arriba).
714
1218
  const pregunta = preguntas[0];
@@ -1102,6 +1606,21 @@ export async function abrirSesionTrueforge(opciones) {
1102
1606
  },
1103
1607
  nuevoHilo(id) {
1104
1608
  hilo = id ?? `tf-${Date.now()}`;
1609
+ // Otra conversación: lo que los especialistas recordaban era de la anterior.
1610
+ memoriaDeEspecialistas.olvidar();
1611
+ esperas.olvidar();
1612
+ vueltasDeCadaHilo.clear();
1613
+ capturasDeCadaHilo.clear();
1614
+ escritosDeCadaHilo.clear();
1615
+ escriturasEnVuelo.clear();
1616
+ nacimientoDeHilo.clear();
1617
+ ultimaEscrituraDeHilo.clear();
1618
+ ultimaCriticaDeHilo.clear();
1619
+ ultimaMedidaDeHilo.clear();
1620
+ rondasVisualesDeHilo.clear();
1621
+ padreDeHilo.clear();
1622
+ informesPendientes.clear();
1623
+ hijosConMemoria.clear();
1105
1624
  // Un hilo NUEVO: lo que hubiera guardado con ese id no se pisa ni se carga a medias. Y una
1106
1625
  // pregunta de la conversación de antes no la contesta el primer mensaje de la nueva.
1107
1626
  const foto = fotoDeLaSesion(hilo);