xonecode 0.19.0 → 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 (61) hide show
  1. package/dist/agent/config/modelos.js +13 -8
  2. package/dist/agent/config/modelos.js.map +1 -1
  3. package/dist/agent/grafo/atributosXone.js +58 -0
  4. package/dist/agent/grafo/atributosXone.js.map +1 -0
  5. package/dist/agent/grafo/xoneAgent.js +3 -2
  6. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  7. package/dist/agent/motores/trueforge/capacidades.js +49 -1
  8. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  9. package/dist/agent/motores/trueforge/esperas.js +61 -0
  10. package/dist/agent/motores/trueforge/esperas.js.map +1 -0
  11. package/dist/agent/motores/trueforge/informesDeHijos.js +63 -0
  12. package/dist/agent/motores/trueforge/informesDeHijos.js.map +1 -0
  13. package/dist/agent/motores/trueforge/medidaAutomatica.js +118 -0
  14. package/dist/agent/motores/trueforge/medidaAutomatica.js.map +1 -0
  15. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js +186 -0
  16. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js.map +1 -0
  17. package/dist/agent/motores/trueforge/resumenSeguro.js +52 -0
  18. package/dist/agent/motores/trueforge/resumenSeguro.js.map +1 -0
  19. package/dist/agent/motores/trueforge/sesionTrueforge.js +485 -27
  20. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  21. package/dist/agent/motores/trueforge/toolsDeFichero.js +69 -3
  22. package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -1
  23. package/dist/agent/subagentes/agentesEnDisco.js +71 -18
  24. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  25. package/dist/agent/turno/diagnosticoDeTools.js +12 -0
  26. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
  27. package/dist/agent/turno/informeDeTraza.js +13 -1
  28. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  29. package/dist/agent/turno/resumenDeTool.js +3 -0
  30. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  31. package/dist/core/agentes.js +7 -1
  32. package/dist/core/agentes.js.map +1 -1
  33. package/dist/core/atributosXone.js +187 -0
  34. package/dist/core/atributosXone.js.map +1 -0
  35. package/dist/core/compararCapturas.js +80 -14
  36. package/dist/core/compararCapturas.js.map +1 -1
  37. package/dist/core/esfuerzo.js +33 -0
  38. package/dist/core/esfuerzo.js.map +1 -1
  39. package/dist/core/iconos.js +6 -1
  40. package/dist/core/iconos.js.map +1 -1
  41. package/dist/core/modoDeEscritura.js +18 -0
  42. package/dist/core/modoDeEscritura.js.map +1 -1
  43. package/dist/core/nucleoXone.js +3 -1
  44. package/dist/core/nucleoXone.js.map +1 -1
  45. package/dist/core/selectorDeCarpeta.js +15 -1
  46. package/dist/core/selectorDeCarpeta.js.map +1 -1
  47. package/dist/core/settings.js +19 -4
  48. package/dist/core/settings.js.map +1 -1
  49. package/dist/core/shellDeAgente.js +39 -0
  50. package/dist/core/shellDeAgente.js.map +1 -1
  51. package/dist/core/tareasDelPlan.js +69 -7
  52. package/dist/core/tareasDelPlan.js.map +1 -1
  53. package/package.json +1 -1
  54. package/skills/xone-development/references/datos/appdata-referencia-ampliada.md +2 -2
  55. package/skills/xone-development/references/datos/appdata.md +5 -3
  56. package/skills/xone-development/references/fundamentos/errores-comunes.md +34 -0
  57. package/skills/xone-hotswap/SKILL.md +2 -0
  58. package/skills/xone-hotswap/scripts/xone-log-android +18 -1
  59. package/skills/xone-plan-builder/SKILL.md +10 -10
  60. package/skills/xone-plan-builder/references/TASKS-FORMAT.md +5 -5
  61. 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,6 +63,9 @@ 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";
@@ -72,6 +79,7 @@ import { crearUnirSecciones } from "../../grafo/unirSecciones.js";
72
79
  import { crearIncorporarAdjunto, recibeIncorporarAdjunto } from "../../grafo/incorporarAdjunto.js";
73
80
  import { crearCriticaVisual } from "../../grafo/criticaVisual.js";
74
81
  import { crearCompararCapturas } from "../../grafo/compararCapturas.js";
82
+ import { crearAtributosXone, NOMBRE_ATRIBUTOS_XONE } from "../../grafo/atributosXone.js";
75
83
  import { crearDiferenciaDeCapturas } from "../../grafo/diferenciaDeCapturas.js";
76
84
  import { crearLectorDeReferencias } from "../../grafo/lectorDeReferencias.js";
77
85
  import { crearTraerDeLaMaquina } from "../../grafo/traerDeLaMaquina.js";
@@ -93,8 +101,8 @@ const BUCLE_DE_CALIDAD = [
93
101
  "encargo, y se comprueba con números y con ojos. Tras cada cambio visual: pide al conductor una captura NATIVA",
94
102
  "nueva y contrástala con `comparar_capturas` y con `xone_critica_visual`, pasando el diseño como `referencia` en las",
95
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",
96
- "diferencias con el diseño —piezas que faltan, tamaños, formas—, delega el arreglo con esas diferencias concretas y",
97
- "repite. Para cuando la estructura coincida y el crítico no señale diferencias con el diseño, o tras TRES vueltas, y",
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",
98
106
  "di lo que sigue faltando en vez de darlo por hecho.",
99
107
  ].join("\n");
100
108
  /**
@@ -103,6 +111,164 @@ const BUCLE_DE_CALIDAD = [
103
111
  * delegación es `create_sub_agent` y no tiene descripción por especialista, así que se traduce
104
112
  * el nombre de la tool y las fichas van escritas aquí, con la MISMA función que deepagents.
105
113
  */
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
+ }
106
272
  export function notaDeDelegacion(agentes, opciones = {}) {
107
273
  if (agentes.length === 0)
108
274
  return "";
@@ -113,6 +279,36 @@ export function notaDeDelegacion(agentes, opciones = {}) {
113
279
  "entiende `create_sub_agent` y `name`. Las fichas de los especialistas:",
114
280
  ...agentes.map((a) => `- ${a.nombre}: ${fichaDeAgente(a, opciones)}`),
115
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
+ : []),
116
312
  ].join("\n");
117
313
  }
118
314
  /** Lo que cada clase NO puede hacer, que la lista de tools no dice sola. */
@@ -125,6 +321,14 @@ const LIMITES_DE = {
125
321
  * La frase de las tools que TIENE, sacada de los nombres que de verdad se le montan: escrita a
126
322
  * mano se quedaba vieja en cuanto se añadía una —la nota decía cuatro y el modelo veía seis—.
127
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.";
128
332
  export function notaDeTools(nombres, limite) {
129
333
  return `NOTA DEL HARNESS: aunque tu identidad diga lo contrario, NO tienes las mismas tools que quien te delega. Tienes ${nombres
130
334
  .map((n) => `\`${n}\``)
@@ -227,6 +431,36 @@ export async function abrirSesionTrueforge(opciones) {
227
431
  * viaja por el cable como nombre de un especialista tiene que serlo de verdad.
228
432
  */
229
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();
230
464
  /** Lo que el agente dejó en `/artefactos/` y aún no se ha anunciado: un artefacto se escribe
231
465
  * SIN aprobación, así que tiene que ANUNCIARSE, como en deepagents. */
232
466
  const artefactosPorAnunciar = [];
@@ -333,25 +567,31 @@ export async function abrirSesionTrueforge(opciones) {
333
567
  ...(carpeta === undefined ? {} : { artefactos: carpeta }),
334
568
  ...(opciones.adjuntos === undefined ? {} : { adjuntos: opciones.adjuntos }),
335
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();
336
585
  const propiasDelRaiz = [
337
586
  navegacion(),
338
- ...(carpeta === undefined
339
- ? []
340
- : [
341
- crearCriticaVisual({
342
- leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)),
343
- invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
344
- // La maqueta casi nunca está en /artefactos/: la trae la persona (/adjuntos/) o vive en /diseno/.
345
- leerReferencia: lectorDeReferencias,
346
- }),
347
- // Medir la estructura contra la maqueta: la otra mitad del crítico, con el raíz como él.
348
- crearCompararCapturas({ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)), leerReferencia: lectorDeReferencias }),
349
- crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto }),
350
- ]),
587
+ atributos,
588
+ ...herramientasDeJuicio(),
589
+ ...(carpeta === undefined ? [] : [crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto })]),
351
590
  ];
352
591
  const propiasDe = (agente) => [
353
592
  crearBusquedaRegex(backend),
354
593
  navegacion(),
594
+ atributos,
355
595
  ...(carpeta !== undefined && (agente.escribeEn ?? []).length > 0
356
596
  ? [crearCopiarArtefacto({ raiz, carpetaDeArtefactos: carpeta, perfil: agente })]
357
597
  : []),
@@ -377,6 +617,10 @@ export async function abrirSesionTrueforge(opciones) {
377
617
  : []),
378
618
  // Y marcar en el plan lo comprobado en el aparato, con el reparto de deepagents: a quien ejecuta.
379
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() : []),
380
624
  ];
381
625
  /**
382
626
  * **Un cliente de modelo por papel, modelo y esfuerzo, que dura la SESIÓN** —hasta `/modelo`—, y
@@ -453,8 +697,37 @@ export async function abrirSesionTrueforge(opciones) {
453
697
  * LECTURA y sin shell: un nombre inventado no tumba el turno ni se lleva la escritura.
454
698
  */
455
699
  const crearHijo = async (params) => {
456
- 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
+ }
457
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());
458
731
  quienEs.set(params.threadId, agente?.nombre ?? params.request.name);
459
732
  if (agente !== undefined)
460
733
  especialistaDeHilo.set(params.threadId, agente.nombre);
@@ -466,6 +739,13 @@ export async function abrirSesionTrueforge(opciones) {
466
739
  backend: backend,
467
740
  propias: propiasDe,
468
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,
469
749
  conShell: () => montarBackend({
470
750
  entorno: entornoDeLaShellDelProyecto(raiz, opciones.artefactos),
471
751
  // El MISMO patrón que ya usa `modeloParaTrueforge` un poco más arriba en este
@@ -475,19 +755,54 @@ export async function abrirSesionTrueforge(opciones) {
475
755
  }),
476
756
  });
477
757
  const nombres = toolsDe(piezas);
478
- 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");
479
762
  const instrucciones = agente === undefined
480
- ? `${nota} Contesta con lo que encuentres y dónde.`
481
- : [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
+ ]
482
773
  .filter((l) => l !== undefined)
483
774
  .join("\n")
484
775
  .trimEnd();
485
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
+ ];
486
801
  const definicionDelHijo = {
487
802
  modelClient: modeloParaTrueforge({
488
803
  modelo: () => agente?.modelo === undefined
489
- ? clienteDe(`papel:${papel}:${agente?.esfuerzo ?? ""}`, () => modelos.paraPapel(papel, agente?.esfuerzo))
490
- : 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)),
491
806
  senal: () => aborto?.signal,
492
807
  alRazonar,
493
808
  soloTexto: () => (detencion.soloTexto(params.threadId) ? RESUMEN_DE_RELLENO : undefined),
@@ -498,11 +813,11 @@ export async function abrirSesionTrueforge(opciones) {
498
813
  anotarPaso("trueforge.detener", `${quienEs.get(params.threadId) ?? params.threadId}: ${n} tool call(s) tiradas`)();
499
814
  },
500
815
  }),
501
- messages: [{ role: "user", content: params.request.input }],
816
+ messages: mensajesIniciales,
502
817
  // Los topes MEDIDOS de deepagents: el conductor más, porque cada paso suyo es un comando.
503
818
  iterationLimit: agente?.ejecucion === true ? TOPE_DE_LLAMADAS_DEL_CONDUCTOR : TOPE_DE_LLAMADAS_DEL_ESPECIALISTA,
504
819
  };
505
- return new AgentThread({
820
+ const hijo = new AgentThread({
506
821
  definition: definicionDelHijo,
507
822
  threadId: params.threadId,
508
823
  title: params.request.name,
@@ -527,6 +842,9 @@ export async function abrirSesionTrueforge(opciones) {
527
842
  tracing: NOOP_AGENT_TRACING,
528
843
  logger,
529
844
  });
845
+ if (agente !== undefined && apertura !== undefined)
846
+ hijosConMemoria.set(params.threadId, { nombre: agente.nombre, hilo: hijo, iniciales: mensajesIniciales });
847
+ return hijo;
530
848
  };
531
849
  /**
532
850
  * El árbol de hilos de UNA conversación. Se rehace con `nuevoHilo`, al reabrir una sesión
@@ -538,9 +856,13 @@ export async function abrirSesionTrueforge(opciones) {
538
856
  /** La pregunta del orquestador que espera respuesta: el siguiente mensaje la contesta. */
539
857
  let preguntaEnEspera;
540
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();
541
863
  const definicion = {
542
864
  modelClient: llm,
543
- instruction: [promptOrquestador(especialistas()), notaDeDelegacion(especialistas(), { conIconos: opciones.iconos !== undefined, conComparacion: carpeta !== undefined })].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"),
544
866
  // Por TURNO, porque el raíz se rehace desde su foto al final de cada uno (ver `turno`).
545
867
  iterationLimit: LIMITE_DE_LLAMADAS_DEL_RAIZ,
546
868
  };
@@ -557,6 +879,7 @@ export async function abrirSesionTrueforge(opciones) {
557
879
  capacidadDeRecortes(backend),
558
880
  capacidadDeFecha(),
559
881
  capacidadDeNotasDeLaSesion,
882
+ capacidadDeInformes,
560
883
  ]),
561
884
  /**
562
885
  * **La conversación se RESUME al mismo umbral que deepagents** (`UMBRAL_RESUMEN_TOKENS`):
@@ -565,7 +888,10 @@ export async function abrirSesionTrueforge(opciones) {
565
888
  * instrucciones van en el prompt de sistema —aquí `instruction`, en un hijo su
566
889
  * capability—, que no se compacta.
567
890
  */
568
- 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
+ }),
569
895
  // `ask_user_question`, SOLO en el raíz —la librería tampoco se la da a un hijo—: es el
570
896
  // único que tiene a una persona delante.
571
897
  askUserQuestion(),
@@ -626,6 +952,39 @@ export async function abrirSesionTrueforge(opciones) {
626
952
  * `thread_id`: mandarla al raíz no la contestaría —o la rechazaría por hilo desconocido—.
627
953
  * Por eso las tool calls se apuntan por hilo Y por id: dos hijos pueden repetir id.
628
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
+ }
629
988
  async function* paso(lote, senal, pendientes, preguntas = []) {
630
989
  for await (const _ of orquestador.send(lote))
631
990
  void _;
@@ -648,13 +1007,22 @@ export async function abrirSesionTrueforge(opciones) {
648
1007
  args = {};
649
1008
  }
650
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());
651
1014
  // La traza: la MISMA lista blanca que el evento (`detalleDe`, `parametrosDe`), nunca
652
1015
  // los argumentos crudos.
653
1016
  const quien = quienEs.get(deHilo) ?? deHilo;
654
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
+ }
655
1022
  // Qué le pide el orquestador a quién: el encargo, acotado (ver `DiagnosticoDeTools.delegacion`).
656
1023
  if (t.function.name === "create_sub_agent" && typeof args["name"] === "string" && typeof args["input"] === "string") {
657
1024
  diagnostico?.delegacion?.(quien, args["name"], args["input"]);
1025
+ esperas.anunciar(args["name"]);
658
1026
  }
659
1027
  }
660
1028
  }
@@ -664,8 +1032,52 @@ export async function abrirSesionTrueforge(opciones) {
664
1032
  const llamada = llamadas.get(claveDe(deHilo, evento.tool_call_id));
665
1033
  const chars = typeof evento.content === "string" ? evento.content.length : 0;
666
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
+ }
667
1046
  }
668
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
+ }
669
1081
  const tope = topeAgotadoDe(evento);
670
1082
  if (tope !== undefined)
671
1083
  diagnostico?.corte?.(quienEs.get(deHilo) ?? deHilo, tope);
@@ -745,6 +1157,7 @@ export async function abrirSesionTrueforge(opciones) {
745
1157
  const memoriaDescartada = avisoDeMemoria;
746
1158
  avisoDeMemoria = undefined;
747
1159
  let cortadoPorTope = false;
1160
+ let preguntasSolas = 0;
748
1161
  let sinResolver = 0;
749
1162
  // El veredicto del turno, con las MISMAS reglas que deepagents (`verificacion.ts`).
750
1163
  let veredicto = "no-corrio";
@@ -770,6 +1183,36 @@ export async function abrirSesionTrueforge(opciones) {
770
1183
  const preguntas = [];
771
1184
  yield* paso(lote, senal, pendientes, preguntas);
772
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
+ }
773
1216
  // El orquestador PREGUNTA: el turno acaba aquí con la pregunta a la vista, y lo que la
774
1217
  // persona escriba después vuelve como la respuesta de esa tool (`turno`, arriba).
775
1218
  const pregunta = preguntas[0];
@@ -1163,6 +1606,21 @@ export async function abrirSesionTrueforge(opciones) {
1163
1606
  },
1164
1607
  nuevoHilo(id) {
1165
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();
1166
1624
  // Un hilo NUEVO: lo que hubiera guardado con ese id no se pisa ni se carga a medias. Y una
1167
1625
  // pregunta de la conversación de antes no la contesta el primer mensaje de la nueva.
1168
1626
  const foto = fotoDeLaSesion(hilo);