xonecode 0.19.0 → 0.20.3

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 (79) hide show
  1. package/apps/web/dist/assets/{index-BvPUqjUm.js → index-40sIfNsT.js} +65 -65
  2. package/apps/web/dist/assets/{index-CGEXMJBx.css → index-BYTna8Os.css} +1 -1
  3. package/apps/web/dist/assets/{php-Dk28VW45.js → php-CDg2Quc0.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-CPzEcmdp.js → ruby-DlFaAWQT.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/config/modelos.js +13 -8
  7. package/dist/agent/config/modelos.js.map +1 -1
  8. package/dist/agent/grafo/atributosXone.js +58 -0
  9. package/dist/agent/grafo/atributosXone.js.map +1 -0
  10. package/dist/agent/grafo/describirImagen.js +134 -0
  11. package/dist/agent/grafo/describirImagen.js.map +1 -0
  12. package/dist/agent/grafo/marcarCriteriosDelPlan.js +3 -2
  13. package/dist/agent/grafo/marcarCriteriosDelPlan.js.map +1 -1
  14. package/dist/agent/grafo/xoneAgent.js +7 -6
  15. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  16. package/dist/agent/motores/trueforge/capacidades.js +49 -1
  17. package/dist/agent/motores/trueforge/capacidades.js.map +1 -1
  18. package/dist/agent/motores/trueforge/esperas.js +61 -0
  19. package/dist/agent/motores/trueforge/esperas.js.map +1 -0
  20. package/dist/agent/motores/trueforge/informesDeHijos.js +63 -0
  21. package/dist/agent/motores/trueforge/informesDeHijos.js.map +1 -0
  22. package/dist/agent/motores/trueforge/medidaAutomatica.js +118 -0
  23. package/dist/agent/motores/trueforge/medidaAutomatica.js.map +1 -0
  24. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js +186 -0
  25. package/dist/agent/motores/trueforge/memoriaDeEspecialistas.js.map +1 -0
  26. package/dist/agent/motores/trueforge/resumenSeguro.js +52 -0
  27. package/dist/agent/motores/trueforge/resumenSeguro.js.map +1 -0
  28. package/dist/agent/motores/trueforge/sesionTrueforge.js +512 -30
  29. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  30. package/dist/agent/motores/trueforge/toolsDeFichero.js +77 -4
  31. package/dist/agent/motores/trueforge/toolsDeFichero.js.map +1 -1
  32. package/dist/agent/subagentes/agentesEnDisco.js +81 -24
  33. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  34. package/dist/agent/turno/diagnosticoDeTools.js +12 -0
  35. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
  36. package/dist/agent/turno/informeDeTraza.js +13 -1
  37. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  38. package/dist/agent/turno/resumenDeTool.js +5 -0
  39. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  40. package/dist/core/actos.js.map +1 -1
  41. package/dist/core/adjuntos.js +2 -3
  42. package/dist/core/adjuntos.js.map +1 -1
  43. package/dist/core/agentes.js +20 -1
  44. package/dist/core/agentes.js.map +1 -1
  45. package/dist/core/atributosXone.js +187 -0
  46. package/dist/core/atributosXone.js.map +1 -0
  47. package/dist/core/compararCapturas.js +81 -15
  48. package/dist/core/compararCapturas.js.map +1 -1
  49. package/dist/core/esfuerzo.js +33 -0
  50. package/dist/core/esfuerzo.js.map +1 -1
  51. package/dist/core/iconos.js +6 -1
  52. package/dist/core/iconos.js.map +1 -1
  53. package/dist/core/modoDeEscritura.js +18 -0
  54. package/dist/core/modoDeEscritura.js.map +1 -1
  55. package/dist/core/motor.js +5 -0
  56. package/dist/core/motor.js.map +1 -1
  57. package/dist/core/nucleoXone.js +3 -1
  58. package/dist/core/nucleoXone.js.map +1 -1
  59. package/dist/core/selectorDeCarpeta.js +15 -1
  60. package/dist/core/selectorDeCarpeta.js.map +1 -1
  61. package/dist/core/settings.js +19 -4
  62. package/dist/core/settings.js.map +1 -1
  63. package/dist/core/shellDeAgente.js +39 -0
  64. package/dist/core/shellDeAgente.js.map +1 -1
  65. package/dist/core/tareasDelPlan.js +70 -8
  66. package/dist/core/tareasDelPlan.js.map +1 -1
  67. package/dist/web/servidor/consolaWeb.js +10 -2
  68. package/dist/web/servidor/consolaWeb.js.map +1 -1
  69. package/dist/web/servidor/pielWeb.js +13 -4
  70. package/dist/web/servidor/pielWeb.js.map +1 -1
  71. package/package.json +1 -1
  72. package/skills/xone-development/references/datos/appdata-referencia-ampliada.md +2 -2
  73. package/skills/xone-development/references/datos/appdata.md +5 -3
  74. package/skills/xone-development/references/fundamentos/errores-comunes.md +34 -0
  75. package/skills/xone-hotswap/SKILL.md +2 -0
  76. package/skills/xone-hotswap/scripts/xone-log-android +18 -1
  77. package/skills/xone-plan-builder/SKILL.md +10 -10
  78. package/skills/xone-plan-builder/references/TASKS-FORMAT.md +5 -5
  79. 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";
@@ -40,7 +42,7 @@ import { createTokenTracker } from "../../../vendor/tokenTracking.js";
40
42
  import { MAX_APPROVAL_ROUNDS } from "../../../vendor/hitl.js";
41
43
  import { backendDeAgente, entornoDeLaShellDelProyecto } from "../../grafo/proyecto.js";
42
44
  import { cargarAgentes } from "../../subagentes/agentesEnDisco.js";
43
- import { fichaDeAgente, promptDeAgente, repartirSkills } from "../../../core/agentes.js";
45
+ import { fichaDeAgente, promptDeAgente, recibeMarcarCriterios, repartirSkills } from "../../../core/agentes.js";
44
46
  import { PERFIL_DEL_ORQUESTADOR, promptOrquestador } from "../../grafo/xoneAgent.js";
45
47
  import { permisosDe, seDetieneEn, TEXTO_HITL } from "../../grafo/perfiles.js";
46
48
  import { cambioDe } from "../../turno/interrupts.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,8 +79,10 @@ 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";
85
+ import { crearDescribirImagen } from "../../grafo/describirImagen.js";
77
86
  import { crearTraerDeLaMaquina } from "../../grafo/traerDeLaMaquina.js";
78
87
  import { invocarVisualConModelos } from "../../dispositivos/juezVisual.js";
79
88
  import { estilosDeDisco, indiceEnDisco } from "../../navegacion/indiceEnDisco.js";
@@ -93,8 +102,8 @@ const BUCLE_DE_CALIDAD = [
93
102
  "encargo, y se comprueba con números y con ojos. Tras cada cambio visual: pide al conductor una captura NATIVA",
94
103
  "nueva y contrástala con `comparar_capturas` y con `xone_critica_visual`, pasando el diseño como `referencia` en las",
95
104
  "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",
105
+ "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",
106
+ "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
107
  "di lo que sigue faltando en vez de darlo por hecho.",
99
108
  ].join("\n");
100
109
  /**
@@ -103,6 +112,174 @@ const BUCLE_DE_CALIDAD = [
103
112
  * delegación es `create_sub_agent` y no tiene descripción por especialista, así que se traduce
104
113
  * el nombre de la tool y las fichas van escritas aquí, con la MISMA función que deepagents.
105
114
  */
115
+ /**
116
+ * **El idioma, dicho desde código**, al raíz y a cada especialista. Visto en la consola web (MyAllXOne): el texto entre
117
+ * herramientas y el razonamiento salían en inglés («Let me call…», «I'll start by reconnoitering…») con la persona
118
+ * escribiendo en español. Nuestros prompts están en español pero ninguno lo decía, y encima hay piezas internas en inglés
119
+ * que tiran de él: la identidad que añade TrueForge («You are the Agent…»), las descripciones de las tools y la costumbre
120
+ * de DeepSeek de razonar en inglés.
121
+ */
122
+ export const IDIOMA_DE_LA_RESPUESTA = "IDIOMA: escribe SIEMPRE en español —lo que dices entre herramientas, tu razonamiento, tus encargos y tus respuestas—, " +
123
+ "aunque algunas instrucciones internas y las descripciones de las herramientas estén en inglés. El código, los nombres de " +
124
+ "ficheros, atributos y funciones, y las citas literales se quedan como son.";
125
+ /**
126
+ * **Al desarrollador no se le cuenta la MAQUINARIA**, solo al raíz, que es quien le habla. Visto en la consola web: al
127
+ * saludar, el raíz explicaba que tenía «la foto del proyecto», que no la iba a «re-escanear», que el especialista lo
128
+ * llevaría «dentro del HANDOFF DE ANÁLISIS» y que usaría `xone_navegacion`. Es cómo trabaja el harness por dentro, no
129
+ * algo que le sirva a quien pide: petición suya. A los hijos no va, porque le hablan al raíz y no a la persona.
130
+ */
131
+ export const SIN_HABLAR_DEL_HARNESS = "CON QUIÉN HABLAS: con un desarrollador de apps XOne, al que le interesa su proyecto y su encargo, no cómo trabajas por " +
132
+ "dentro. No le nombres tus herramientas, a tus especialistas por su nombre interno, los protocolos entre ellos (HANDOFF " +
133
+ "DE ANÁLISIS…), la foto o el contexto que te llega al empezar, ni lo que vas a medir o no: cuéntale lo que encontraste, " +
134
+ "lo que hiciste en su proyecto y lo que necesitas de él. Si pregunta cómo trabajas, entonces sí.";
135
+ /** Lo que recibe el agente en modo autónomo cuando pregunta sin marcar ninguna opción como recomendada. */
136
+ 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é.";
137
+ /** Un comando que saca una captura de pantalla del aparato. */
138
+ export const COMANDO_DE_CAPTURA = /xone-captura-android|xone-hotswap\s+shot\b|\bscreencap\b/;
139
+ /** Con cuántas capturas de UN encargo se le avisa a quien ejecuta. */
140
+ export const UMBRALES_DE_CAPTURAS = [2, 4];
141
+ /**
142
+ * El aviso de capturas. El prompt del conductor ya decía que una captura «no es una herramienta de diagnóstico», y una
143
+ * comprobación de una pasada real sacó 18 —61 ficheros y 92 pares casi iguales en total—, casi todas para mirar valores
144
+ * y excepciones que `getText`/`getFields` y el log contestan sin mirar nada. Un texto no es un contador.
145
+ */
146
+ export function textoDeCapturas(capturas) {
147
+ return [
148
+ `LLEVAS ${String(capturas)} CAPTURAS en este encargo.`,
149
+ "Para un VALOR, un estado o una excepción no hace falta ninguna: `xone-log-android --app` (lo que la app escribió con",
150
+ "`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",
151
+ "cosa que compruebes, con `shot name=<qué pruebas>`. Si solo quieres saber si algo cambió, `diferencia_de_capturas`.",
152
+ "No repitas la misma pantalla: si la anterior ya lo decía, no hay nada nuevo que sacar.",
153
+ ].join("\n");
154
+ }
155
+ /** Con cuántas comprobaciones seguidas se le avisa a quien lleva un lazo. El primero es el «tres vueltas» del prompt. */
156
+ export const UMBRALES_DE_VUELTAS = [3, 5];
157
+ /**
158
+ * El aviso: no dice «para», dice «cambia de procedimiento». La misma familia de fallo se vio arreglada de cinco
159
+ * maneras (un control que falta, y al arreglarlo, otro): lo que hay que hacer es aislar la causa una vez y aplicarla
160
+ * a todos los sitios, no reescribir otra vez.
161
+ */
162
+ export function textoDeVueltas(vueltas) {
163
+ return [
164
+ `LLEVAS ${String(vueltas)} COMPROBACIONES SEGUIDAS con el aparato.`,
165
+ "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",
166
+ "reescritura: (1) di la hipótesis en una frase, (2) pide al de pruebas UN experimento mínimo —un control, un evento— y lee",
167
+ "el resultado BRUTO del aparato, (3) con la causa confirmada, aplica el arreglo a TODOS los sitios parecidos de una vez.",
168
+ "Si tras eso el fallo sigue igual, devuelve el trabajo diciendo qué falta y qué mediste, en vez de otra vuelta.",
169
+ ].join("\n");
170
+ }
171
+ /**
172
+ * El hilo de un especialista que NO arrancó porque espera a otros: nace ya terminado, con la respuesta al orquestador
173
+ * ya escrita (`preComputedCompletion`), y la librería lo trata como cualquier hijo que acaba. No llama al modelo ni tiene
174
+ * tools: lo único que hace es contestar al que lo pidió.
175
+ */
176
+ function hiloQueNoArranca(nombre, esperados, params, modelo, logger) {
177
+ const padre = params.parent;
178
+ const texto = `${nombre} NO HA ARRANCADO: espera a que termine ${esperados.join(", ")}, que sigue trabajando y produce lo que ${nombre} necesita. ` +
179
+ `Cuando ${esperados.join(", ")} te devuelva su informe, vuelve a llamar a ${nombre} con ese informe dentro del encargo.`;
180
+ return new AgentThread({
181
+ definition: { modelClient: modelo, iterationLimit: 1 },
182
+ threadId: params.threadId,
183
+ title: params.request.name,
184
+ parent: params.parent,
185
+ agentInfo: { type: "dynamic", ...params.request },
186
+ preComputedCompletion: {
187
+ type: "done",
188
+ output: { role: "assistant", content: texto },
189
+ send_to_parent: { role: "tool", tool_call_id: padre.tool_call_id ?? "", content: texto },
190
+ },
191
+ tracing: NOOP_AGENT_TRACING,
192
+ logger: logger,
193
+ });
194
+ }
195
+ /**
196
+ * Lo que se le cuenta a quien ESPERÓ a otros especialistas, al soltarse: a quién esperó y qué hay en el disco.
197
+ * Solo NOMBRES —los recursos de `icons/` y los `ASSETS.md`/`TASKS.md` del plan—, nunca su contenido: es lo que dejó
198
+ * el otro, que no viaja por ningún canal más que el disco. Sin recursos que nombrar, un aviso corto.
199
+ */
200
+ export function textoDeLoQueDejaronLosEsperados(raiz, esperadas) {
201
+ let recursos = [];
202
+ try {
203
+ recursos = readdirSync(join(raiz, "icons")).filter((f) => /\.(svg|png|jpg|jpeg)$/i.test(f)).sort();
204
+ }
205
+ catch {
206
+ recursos = [];
207
+ }
208
+ let notas = [];
209
+ try {
210
+ notas = readdirSync(join(raiz, ".xonecode", "planes"))
211
+ .flatMap((p) => ["ASSETS.md"].filter((f) => existsSync(join(raiz, ".xonecode", "planes", p, f))).map((f) => `/planes/${p}/${f}`))
212
+ .sort();
213
+ }
214
+ catch {
215
+ notas = [];
216
+ }
217
+ return [
218
+ `Antes de ti han terminado ${esperadas.join(", ")}, que trabajaban a la vez o antes que tú.`,
219
+ ...(recursos.length === 0 ? [] : [`Lo que hay ahora en icons/ (${String(recursos.length)}): ${recursos.join(", ")}.`]),
220
+ ...(notas.length === 0 ? [] : [`Lo que dejó escrito para ti: ${notas.join(", ")}. Léelo antes de escribir.`]),
221
+ "USA los recursos que te sirvan por su nombre exacto, y si no vas a usar alguno, dilo al devolver el trabajo.",
222
+ ].join("\n");
223
+ }
224
+ /**
225
+ * 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
226
+ * escribas (aunque recuerde sus encargos anteriores de la sesión), y lo que te devuelve es lo que vio, no un
227
+ * veredicto: decidir si lo que escribiste funciona sigue siendo cosa tuya, con lo que ese hijo te traiga.
228
+ */
229
+ export function textoDelBucle(llama, opciones = {}) {
230
+ const puedeDiseñar = llama.includes("designer-xone");
231
+ return [
232
+ `PUEDES LLAMAR A: ${llama.join(", ")}, con \`create_sub_agent\` (\`name\` exacto y \`input\` autosuficiente).`,
233
+ "Tu bucle es: escribe, llama al de pruebas para desplegar y comprobar, lee lo que te devuelve y corrige.",
234
+ "Dile QUÉ comprobar y qué esperas ver. No lo llames por cada línea que cambies: junta los cambios.",
235
+ "",
236
+ "PRUEBAS FUNCIONALES Y VISUALES, SEPARADAS Y EN ESTE ORDEN:",
237
+ "- Instrumenta tu código con `console.log(\"CALC tecla 8\")`, `console.log(\"CALC resultado \" + r)` en los puntos",
238
+ " que deciden (tecla, expresión, resultado, error). Nunca `ui.showToast` ni `appData.writeConsoleString` para depurar:",
239
+ " el aviso no deja rastro y el segundo es legacy y no llega al log de Android.",
240
+ "- FUNCIONAL: UN encargo con TODOS los casos, como acciones y lo que esperas («teclea 2 + 3 × 4 = → log `CALC resultado",
241
+ " 14`, display 14»). El de pruebas los hace, lee el log con `xone-log-android --app` y te devuelve cada caso: pasa o falla",
242
+ " con lo que salió. SIN capturas. Corrige todos los fallos juntos y repite solo los que fallaron.",
243
+ "- VISUAL, solo cuando lo funcional pasa: UNA captura por ronda. Con ella el harness te trae la medida contra la maqueta;",
244
+ " pasa el crítico, arregla TODAS las diferencias en la misma ronda y solo entonces pide otra captura. Tres rondas como mucho.",
245
+ "- Una pantalla que se parece a la maqueta con datos de ejemplo fijos NO está terminada: la lógica tiene que funcionar.",
246
+ "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.",
247
+ "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",
248
+ "—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",
249
+ "(el teclado, las funciones, el historial). Un fallo de arranque descubierto con la pantalla entera escrita cuesta reescribirla.",
250
+ ...(puedeDiseñar
251
+ ? [
252
+ "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",
253
+ "encargó. `designer-xone` solo hace RECURSOS —iconos de una biblioteca y fondos SVG en `icons/`—: pídeselos juntos, con para",
254
+ "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.",
255
+ "Los recursos te llegan con una TABLA (fichero → control → atributo → tamaño → si lleva el símbolo dentro): úsala y no abras",
256
+ "los SVG para enterarte de lo que ya dice.",
257
+ ]
258
+ : []),
259
+ ...(opciones.conCritica === true
260
+ ? [
261
+ "",
262
+ "PARA JUZGAR UNA PANTALLA tienes `xone_critica_visual` (un modelo aparte mira la captura) y `comparar_capturas` (mide la",
263
+ "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,",
264
+ "si hay diseño (/diseno/ o un adjunto), como `referencia`. Lo que digan esas dos herramientas manda sobre tu impresión: no",
265
+ "lo descartes como «no fiable»; si no estás de acuerdo, dilo con el dato.",
266
+ "Cada vez que vuelva el de pruebas con una captura, el HARNESS te trae su medida contra la maqueta: no hace falta que",
267
+ "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.",
268
+ ]
269
+ : []),
270
+ "",
271
+ "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",
272
+ "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",
273
+ "`comparar_capturas` por debajo del 10 % en vertical y en horizontal, que ENCAJE en la pantalla (cada borde del contenido a",
274
+ "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.",
275
+ "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",
276
+ "las animaciones; una sombra o un degradado se aproximan con un SVG. Tienes TRES vueltas de corregir y volver a comprobar para",
277
+ "llegar; si al agotarlas algo",
278
+ "sigue sin cumplirse, devuelve el trabajo diciendo QUÉ falta, con lo que midieron el crítico y la medida tal cual. Devolver antes",
279
+ "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",
280
+ "lo revisa igual.",
281
+ ].join("\n");
282
+ }
106
283
  export function notaDeDelegacion(agentes, opciones = {}) {
107
284
  if (agentes.length === 0)
108
285
  return "";
@@ -113,6 +290,36 @@ export function notaDeDelegacion(agentes, opciones = {}) {
113
290
  "entiende `create_sub_agent` y `name`. Las fichas de los especialistas:",
114
291
  ...agentes.map((a) => `- ${a.nombre}: ${fichaDeAgente(a, opciones)}`),
115
292
  ...(opciones.conComparacion === true ? ["", BUCLE_DE_CALIDAD] : []),
293
+ ...(opciones.conEsperas === true
294
+ ? [
295
+ "",
296
+ "ESPERAS ENTRE ESPECIALISTAS: developer-xone NO arranca mientras designer-xone esté trabajando (lo que el diseñador produce",
297
+ "—los recursos de `icons/`— lo necesita el `.xne`). Si los lanzas a la vez, la llamada al desarrollador te vuelve AL INSTANTE",
298
+ "diciendo que no ha arrancado; cuando el diseñador te devuelva su informe, llama de nuevo al desarrollador con ESE INFORME dentro",
299
+ "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.",
300
+ ]
301
+ : []),
302
+ ...(opciones.conBucle === true
303
+ ? [
304
+ "",
305
+ "BUCLE DEL DESARROLLADOR: developer-xone prueba lo que escribe él mismo, llamando a device-controller (despliega, toca y lee",
306
+ "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",
307
+ "cada encargo a developer-xone lleva el CRITERIO ESCRITO y MEDIBLE: qué casillas del plan tiene que dejar comprobadas, qué",
308
+ "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",
309
+ "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",
310
+ "lo que XOne no reproduce —las tipografías del diseño, el desenfoque de fondo, las animaciones—. Sin criterio, devolverá el",
311
+ "trabajo en cuanto funcione. No le encargues a device-controller comprobar SU",
312
+ "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",
313
+ "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.",
314
+ ]
315
+ : []),
316
+ ...(opciones.conMemoria === true
317
+ ? [
318
+ "",
319
+ "MEMORIA DE LOS ESPECIALISTAS: en esta sesión cada especialista RECUERDA sus encargos anteriores (lo que leyó,",
320
+ "lo que descubrió y lo que hizo). Al volver a llamar a uno, dale solo lo NUEVO: no repitas lo que ya sabe.",
321
+ ]
322
+ : []),
116
323
  ].join("\n");
117
324
  }
118
325
  /** Lo que cada clase NO puede hacer, que la lista de tools no dice sola. */
@@ -125,6 +332,14 @@ const LIMITES_DE = {
125
332
  * La frase de las tools que TIENE, sacada de los nombres que de verdad se le montan: escrita a
126
333
  * mano se quedaba vieja en cuanto se añadía una —la nota decía cuatro y el modelo veía seis—.
127
334
  */
335
+ /**
336
+ * 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
337
+ * montada y descrita, el consultor la consultó 9 veces y el desarrollador —quien escribe los atributos— ninguna, con 13
338
+ * 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
339
+ * `SKILL.md` porque ese llega también a los motores externos, que no tienen la tool.
340
+ */
341
+ 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`: " +
342
+ "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
343
  export function notaDeTools(nombres, limite) {
129
344
  return `NOTA DEL HARNESS: aunque tu identidad diga lo contrario, NO tienes las mismas tools que quien te delega. Tienes ${nombres
130
345
  .map((n) => `\`${n}\``)
@@ -227,6 +442,36 @@ export async function abrirSesionTrueforge(opciones) {
227
442
  * viaja por el cable como nombre de un especialista tiene que serlo de verdad.
228
443
  */
229
444
  const especialistaDeHilo = new Map();
445
+ const conMemoriaDeEspecialistas = opciones.memoriaDeEspecialistas ?? process.env.XONECODE_SIN_MEMORIA_DE_ESPECIALISTAS !== "1";
446
+ // Encendido por omisión desde que se midió (calc9-calc15); `XONECODE_BUCLE_DEL_DEVELOPER=0` lo apaga para comparar.
447
+ const conBucleDelDesarrollador = opciones.bucleDelDesarrollador ?? process.env.XONECODE_BUCLE_DEL_DEVELOPER !== "0";
448
+ const conEsperas = opciones.esperasEntreHijos ?? true;
449
+ /** Cuántas comprobaciones (llamadas a quien EJECUTA) lleva cada hilo que lleva un lazo. */
450
+ const vueltasDeCadaHilo = new Map();
451
+ /** Cuántas CAPTURAS ha sacado cada hilo que ejecuta comandos (`xone-captura-android`, `xone-hotswap shot`). */
452
+ const capturasDeCadaHilo = new Map();
453
+ /** Lo que escribió cada hijo en su encargo, quién lo llamó, y los informes que esperan a ese padre (`informesDeHijos.ts`). */
454
+ const escritosDeCadaHilo = new Map();
455
+ const padreDeHilo = new Map();
456
+ const informesPendientes = new Map();
457
+ /** Las escrituras pedidas y aún sin respuesta, por hilo e id: la respuesta de una aprobada llega en OTRO `paso`. */
458
+ const escriturasEnVuelo = new Map();
459
+ /** 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ó. */
460
+ const nacimientoDeHilo = new Map();
461
+ const ultimaEscrituraDeHilo = new Map();
462
+ const ultimaCriticaDeHilo = new Map();
463
+ const ultimaMedidaDeHilo = new Map();
464
+ /** Cuántas rondas VISUALES lleva cada hilo: una por medida automática recibida (`RONDAS_VISUALES`). */
465
+ const rondasVisualesDeHilo = new Map();
466
+ const capacidadDeInformes = capacidadDeInformesDeHijos((hiloPadre) => {
467
+ const textos = informesPendientes.get(hiloPadre) ?? [];
468
+ informesPendientes.delete(hiloPadre);
469
+ return textos;
470
+ });
471
+ const esperas = crearEsperas();
472
+ const memoriaDeEspecialistas = crearMemoriaDeEspecialistas();
473
+ /** Los hijos con memoria que corren ahora: su hilo, para leer su historial al terminar. */
474
+ const hijosConMemoria = new Map();
230
475
  /** Lo que el agente dejó en `/artefactos/` y aún no se ha anunciado: un artefacto se escribe
231
476
  * SIN aprobación, así que tiene que ANUNCIARSE, como en deepagents. */
232
477
  const artefactosPorAnunciar = [];
@@ -333,25 +578,44 @@ export async function abrirSesionTrueforge(opciones) {
333
578
  ...(carpeta === undefined ? {} : { artefactos: carpeta }),
334
579
  ...(opciones.adjuntos === undefined ? {} : { adjuntos: opciones.adjuntos }),
335
580
  });
581
+ /** El crítico y la medida, con las MISMAS piezas para quien las tenga: el raíz, y con el bucle encendido el desarrollador. */
582
+ const herramientasDeJuicio = () => carpeta === undefined
583
+ ? []
584
+ : [
585
+ crearCriticaVisual({
586
+ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)),
587
+ invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
588
+ // La maqueta casi nunca está en /artefactos/: la trae la persona (/adjuntos/) o vive en /diseno/.
589
+ leerReferencia: lectorDeReferencias,
590
+ }),
591
+ // Medir la estructura contra la maqueta: la otra mitad del crítico, con el raíz como él.
592
+ crearCompararCapturas({ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)), leerReferencia: lectorDeReferencias }),
593
+ ];
594
+ // ¿Existe este atributo en este nodo? A todos, como la navegación: es de lectura y contesta desde la skill del paquete.
595
+ const atributos = crearAtributosXone();
596
+ // VER una imagen (IXCODE-23), a TODOS, el raíz incluido: es de lectura, y el raíz es quien recibe los adjuntos.
597
+ // `read_file` no abre una imagen y el aviso de adjuntos manda aquí (`describirImagen.ts`).
598
+ const describirImagen = () => crearDescribirImagen({
599
+ invocar: invocarVisualConModelos({ paraPapel: (p) => modelos.paraPapel(p) }),
600
+ leer: lectorDeReferencias,
601
+ carpetas: {
602
+ raiz,
603
+ ...(carpeta === undefined ? {} : { artefactos: carpeta }),
604
+ ...(opciones.adjuntos === undefined ? {} : { adjuntos: opciones.adjuntos }),
605
+ },
606
+ });
336
607
  const propiasDelRaiz = [
337
608
  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
- ]),
609
+ atributos,
610
+ describirImagen(),
611
+ ...herramientasDeJuicio(),
612
+ ...(carpeta === undefined ? [] : [crearTraerDeLaMaquina({ carpeta, alEscribir: anotarArtefacto })]),
351
613
  ];
352
614
  const propiasDe = (agente) => [
353
615
  crearBusquedaRegex(backend),
354
616
  navegacion(),
617
+ atributos,
618
+ describirImagen(),
355
619
  ...(carpeta !== undefined && (agente.escribeEn ?? []).length > 0
356
620
  ? [crearCopiarArtefacto({ raiz, carpetaDeArtefactos: carpeta, perfil: agente })]
357
621
  : []),
@@ -375,8 +639,12 @@ export async function abrirSesionTrueforge(opciones) {
375
639
  ...(carpeta !== undefined && agente.ejecucion === true
376
640
  ? [crearDiferenciaDeCapturas({ leerArtefacto: async (nombre) => readFileSync(join(carpeta, nombre)) })]
377
641
  : []),
378
- // Y marcar en el plan lo comprobado en el aparato, con el reparto de deepagents: a quien ejecuta.
379
- ...(agente.ejecucion === true ? [crearMarcarCriteriosDelPlan({ raiz })] : []),
642
+ // Y marcar en el plan lo comprobado, con el reparto de deepagents (`recibeMarcarCriterios`).
643
+ ...(recibeMarcarCriterios(agente) ? [crearMarcarCriteriosDelPlan({ raiz })] : []),
644
+ // El BUCLE del desarrollador: quien puede llamar al de pruebas también tiene el crítico y la medida, para
645
+ // juzgar la pantalla con lo que el otro capture. El veredicto sigue siendo de un modelo aparte, no suyo, y el
646
+ // juez final del arnés revisa igual (`textoDelBucle`).
647
+ ...(conBucleDelDesarrollador && (agente.llama?.length ?? 0) > 0 ? herramientasDeJuicio() : []),
380
648
  ];
381
649
  /**
382
650
  * **Un cliente de modelo por papel, modelo y esfuerzo, que dura la SESIÓN** —hasta `/modelo`—, y
@@ -453,8 +721,37 @@ export async function abrirSesionTrueforge(opciones) {
453
721
  * LECTURA y sin shell: un nombre inventado no tumba el turno ni se lleva la escritura.
454
722
  */
455
723
  const crearHijo = async (params) => {
456
- const agente = especialistas().find((a) => a.nombre === params.request.name);
724
+ // Quién lo pide: el raíz, o un especialista que llama a otro. Un especialista solo puede llamar a los que su
725
+ // `.md` declara en `llama` y con el interruptor puesto; lo demás cae al ayudante genérico de solo lectura,
726
+ // el mismo de un nombre inventado, y se anota en la traza.
727
+ const quienPide = params.parent === undefined ? undefined : especialistaDeHilo.get(params.parent.thread_id ?? "");
728
+ const permitidos = quienPide === undefined ? undefined : (especialistas().find((a) => a.nombre === quienPide)?.llama ?? []);
729
+ const concedido = permitidos === undefined || (conBucleDelDesarrollador && permitidos.includes(params.request.name));
730
+ if (!concedido)
731
+ anotarPaso("trueforge.llamada", `${quienPide ?? "?"} pidió a ${params.request.name}: no está en su lista`)();
732
+ const agente = concedido ? especialistas().find((a) => a.nombre === params.request.name) : undefined;
733
+ // Cuenta una COMPROBACIÓN: quien lleva un lazo llamó a quien ejecuta. Es lo que dispara el aviso de vueltas.
734
+ if (concedido && agente?.ejecucion === true && params.parent !== undefined) {
735
+ const padre = params.parent.thread_id ?? "";
736
+ if (padre !== "")
737
+ vueltasDeCadaHilo.set(padre, (vueltasDeCadaHilo.get(padre) ?? 0) + 1);
738
+ }
739
+ // ¿Arranca? Un especialista que declara `espera` NO arranca mientras haya vivo —o anunciado por el mismo mensaje del
740
+ // orquestador— uno de esos: se le devuelve al instante diciéndolo, y el orquestador lo llama de nuevo con el informe del
741
+ // otro. No se espera bloqueando: bloqueaba a la librería, que no devuelve el control mientras un hilo no termine su
742
+ // paso, y una aprobación del otro hilo no se atendía (`esperas.ts`).
743
+ const debeEsperar = conEsperas && agente !== undefined ? esperas.vivos(agente.espera ?? [], params.threadId) : [];
744
+ if (agente !== undefined && debeEsperar.length > 0) {
745
+ anotarPaso("trueforge.espera", `${agente.nombre} no arranca: espera a ${debeEsperar.join(", ")}`)();
746
+ diagnostico?.memoria?.(agente.nombre, "arranca-sin", `espera a ${debeEsperar.join(", ")}`);
747
+ return hiloQueNoArranca(agente.nombre, debeEsperar, params, llm, logger);
748
+ }
457
749
  detencion.nacio(params.threadId);
750
+ esperas.nacio(agente?.nombre ?? params.request.name, params.threadId);
751
+ const hiloDelPadre = params.parent?.thread_id;
752
+ if (hiloDelPadre !== undefined && hiloDelPadre !== "")
753
+ padreDeHilo.set(params.threadId, hiloDelPadre);
754
+ nacimientoDeHilo.set(params.threadId, Date.now());
458
755
  quienEs.set(params.threadId, agente?.nombre ?? params.request.name);
459
756
  if (agente !== undefined)
460
757
  especialistaDeHilo.set(params.threadId, agente.nombre);
@@ -466,6 +763,13 @@ export async function abrirSesionTrueforge(opciones) {
466
763
  backend: backend,
467
764
  propias: propiasDe,
468
765
  notas: capacidadDeNotasDeLaSesion,
766
+ puedeLlamar: (a) => conBucleDelDesarrollador && (a.llama?.length ?? 0) > 0,
767
+ informes: capacidadDeInformes,
768
+ vueltas: (a) => a.ejecucion === true
769
+ ? capacidadDeAvisoDeVueltas({ vueltasDe: (hilo) => capturasDeCadaHilo.get(hilo) ?? 0, umbrales: UMBRALES_DE_CAPTURAS, texto: textoDeCapturas })
770
+ : conBucleDelDesarrollador && (a.llama?.length ?? 0) > 0
771
+ ? capacidadDeAvisoDeVueltas({ vueltasDe: (hilo) => vueltasDeCadaHilo.get(hilo) ?? 0, umbrales: UMBRALES_DE_VUELTAS, texto: textoDeVueltas })
772
+ : undefined,
469
773
  conShell: () => montarBackend({
470
774
  entorno: entornoDeLaShellDelProyecto(raiz, opciones.artefactos),
471
775
  // El MISMO patrón que ya usa `modeloParaTrueforge` un poco más arriba en este
@@ -475,19 +779,54 @@ export async function abrirSesionTrueforge(opciones) {
475
779
  }),
476
780
  });
477
781
  const nombres = toolsDe(piezas);
478
- const nota = notaDeTools(nombres, agente === undefined ? LIMITE_DEL_GENERICO : LIMITES_DE[clase]);
782
+ const nota = [
783
+ notaDeTools(nombres, agente === undefined ? LIMITE_DEL_GENERICO : LIMITES_DE[clase]),
784
+ ...(nombres.includes(NOMBRE_ATRIBUTOS_XONE) ? [USO_DE_XONE_ATRIBUTOS] : []),
785
+ ].join("\n");
479
786
  const instrucciones = agente === undefined
480
- ? `${nota} Contesta con lo que encuentres y dónde.`
481
- : [promptDeAgente(agente, repartirSkills(agente, disponibles)), "", nota, anuncioDeSkills(agente, catalogo)]
787
+ ? `${IDIOMA_DE_LA_RESPUESTA}\n\n${nota} Contesta con lo que encuentres y dónde.`
788
+ : [
789
+ IDIOMA_DE_LA_RESPUESTA,
790
+ "",
791
+ promptDeAgente(agente, repartirSkills(agente, disponibles)),
792
+ "",
793
+ nota,
794
+ anuncioDeSkills(agente, catalogo),
795
+ ...(conBucleDelDesarrollador && (agente.llama?.length ?? 0) > 0 ? ["", textoDelBucle(agente.llama ?? [], { conCritica: carpeta !== undefined })] : []),
796
+ ]
482
797
  .filter((l) => l !== undefined)
483
798
  .join("\n")
484
799
  .trimEnd();
485
800
  const papel = agente?.soloLectura === true ? "rapido" : "trabajo";
801
+ const claseDeEsfuerzo = agente === undefined ? undefined : claseDeTrabajo(agente);
802
+ // La memoria del especialista: su conversación anterior de ESTA sesión, si la hay y se puede usar.
803
+ const apertura = conMemoriaDeEspecialistas && agente !== undefined ? memoriaDeEspecialistas.abrir(agente.nombre, params.threadId) : undefined;
804
+ const previa = apertura !== undefined && "previa" in apertura ? apertura.previa : undefined;
805
+ if (agente !== undefined && apertura !== undefined) {
806
+ if (previa !== undefined)
807
+ diagnostico?.memoria?.(agente.nombre, "arranca-con", "historial de la sesión", previa.tokens);
808
+ else
809
+ diagnostico?.memoria?.(agente.nombre, "arranca-sin", "sin" in apertura ? apertura.sin : "primera");
810
+ }
811
+ // Si lo que esperaba ya terminó en esta conversación, sabe qué dejó (solo nombres, nunca contenido).
812
+ const esperado = conEsperas && agente !== undefined ? esperas.terminados(agente.espera ?? []) : [];
813
+ const avisoDeLoEsperado = esperado.length > 0 ? `${textoDeLoQueDejaronLosEsperados(raiz, esperado)}\n\n` : "";
814
+ const mensajesIniciales = previa === undefined
815
+ ? [{ role: "user", content: `${avisoDeLoEsperado}${params.request.input}` }]
816
+ : [
817
+ ...previa.mensajes,
818
+ {
819
+ role: "user",
820
+ 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. " +
821
+ "No repitas lecturas de ficheros que no hayan cambiado.\n\n" +
822
+ `${avisoDeLoEsperado}${params.request.input}`,
823
+ },
824
+ ];
486
825
  const definicionDelHijo = {
487
826
  modelClient: modeloParaTrueforge({
488
827
  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)),
828
+ ? clienteDe(`papel:${papel}:${agente?.esfuerzo ?? ""}:${claseDeEsfuerzo ?? ""}:${agente?.pensamiento ?? ""}`, () => modelos.paraPapel(papel, agente?.esfuerzo, claseDeEsfuerzo, agente?.pensamiento))
829
+ : clienteDe(`modelo:${agente.modelo}:${agente.esfuerzo ?? ""}:${claseDeEsfuerzo ?? ""}:${agente.pensamiento ?? ""}`, () => modelos.paraModelo(agente.modelo, agente.esfuerzo, claseDeEsfuerzo, agente.pensamiento)),
491
830
  senal: () => aborto?.signal,
492
831
  alRazonar,
493
832
  soloTexto: () => (detencion.soloTexto(params.threadId) ? RESUMEN_DE_RELLENO : undefined),
@@ -498,11 +837,11 @@ export async function abrirSesionTrueforge(opciones) {
498
837
  anotarPaso("trueforge.detener", `${quienEs.get(params.threadId) ?? params.threadId}: ${n} tool call(s) tiradas`)();
499
838
  },
500
839
  }),
501
- messages: [{ role: "user", content: params.request.input }],
840
+ messages: mensajesIniciales,
502
841
  // Los topes MEDIDOS de deepagents: el conductor más, porque cada paso suyo es un comando.
503
842
  iterationLimit: agente?.ejecucion === true ? TOPE_DE_LLAMADAS_DEL_CONDUCTOR : TOPE_DE_LLAMADAS_DEL_ESPECIALISTA,
504
843
  };
505
- return new AgentThread({
844
+ const hijo = new AgentThread({
506
845
  definition: definicionDelHijo,
507
846
  threadId: params.threadId,
508
847
  title: params.request.name,
@@ -527,6 +866,9 @@ export async function abrirSesionTrueforge(opciones) {
527
866
  tracing: NOOP_AGENT_TRACING,
528
867
  logger,
529
868
  });
869
+ if (agente !== undefined && apertura !== undefined)
870
+ hijosConMemoria.set(params.threadId, { nombre: agente.nombre, hilo: hijo, iniciales: mensajesIniciales });
871
+ return hijo;
530
872
  };
531
873
  /**
532
874
  * El árbol de hilos de UNA conversación. Se rehace con `nuevoHilo`, al reabrir una sesión
@@ -538,9 +880,13 @@ export async function abrirSesionTrueforge(opciones) {
538
880
  /** La pregunta del orquestador que espera respuesta: el siguiente mensaje la contesta. */
539
881
  let preguntaEnEspera;
540
882
  const nuevoOrquestador = (foto) => {
883
+ // Los hijos que corrían no pueden seguir; lo que aprendieron sí se queda.
884
+ memoriaDeEspecialistas.darPorMuertos();
885
+ esperas.darPorMuertos();
886
+ hijosConMemoria.clear();
541
887
  const definicion = {
542
888
  modelClient: llm,
543
- instruction: [promptOrquestador(especialistas()), notaDeDelegacion(especialistas(), { conIconos: opciones.iconos !== undefined, conComparacion: carpeta !== undefined })].filter((l) => l !== "").join("\n\n"),
889
+ instruction: [IDIOMA_DE_LA_RESPUESTA, SIN_HABLAR_DEL_HARNESS, promptOrquestador(especialistas()), notaDeDelegacion(especialistas(), { conIconos: opciones.iconos !== undefined, conComparacion: carpeta !== undefined, conMemoria: conMemoriaDeEspecialistas, conBucle: conBucleDelDesarrollador, conEsperas })].filter((l) => l !== "").join("\n\n"),
544
890
  // Por TURNO, porque el raíz se rehace desde su foto al final de cada uno (ver `turno`).
545
891
  iterationLimit: LIMITE_DE_LLAMADAS_DEL_RAIZ,
546
892
  };
@@ -557,6 +903,7 @@ export async function abrirSesionTrueforge(opciones) {
557
903
  capacidadDeRecortes(backend),
558
904
  capacidadDeFecha(),
559
905
  capacidadDeNotasDeLaSesion,
906
+ capacidadDeInformes,
560
907
  ]),
561
908
  /**
562
909
  * **La conversación se RESUME al mismo umbral que deepagents** (`UMBRAL_RESUMEN_TOKENS`):
@@ -565,7 +912,10 @@ export async function abrirSesionTrueforge(opciones) {
565
912
  * instrucciones van en el prompt de sistema —aquí `instruction`, en un hijo su
566
913
  * capability—, que no se compacta.
567
914
  */
568
- contextCompaction({ definition: definicion, compactionThresholdTokens: UMBRAL_RESUMEN_TOKENS }),
915
+ contextCompaction({
916
+ definition: { ...definicion, modelClient: conResumenSeguro(definicion.modelClient, (t) => anotarPaso("trueforge.compactacion", t)()) },
917
+ compactionThresholdTokens: UMBRAL_RESUMEN_TOKENS,
918
+ }),
569
919
  // `ask_user_question`, SOLO en el raíz —la librería tampoco se la da a un hijo—: es el
570
920
  // único que tiene a una persona delante.
571
921
  askUserQuestion(),
@@ -626,6 +976,39 @@ export async function abrirSesionTrueforge(opciones) {
626
976
  * `thread_id`: mandarla al raíz no la contestaría —o la rechazaría por hilo desconocido—.
627
977
  * Por eso las tool calls se apuntan por hilo Y por id: dos hijos pueden repetir id.
628
978
  */
979
+ /**
980
+ * Cuando vuelve quien EJECUTA, la medida de su última captura contra la maqueta, para quien lo llamó
981
+ * (`medidaAutomatica.ts`). Sin carpeta de artefactos, sin maqueta o sin captura nueva, nada. Un fallo al abrir o
982
+ * decodificar se anota y no tumba el turno.
983
+ */
984
+ function medidaTrasProbar(deHilo, hiloPadre, quien) {
985
+ if (carpeta === undefined || quien?.ejecucion !== true)
986
+ return undefined;
987
+ const maqueta = buscarMaqueta(raiz, opciones.adjuntos);
988
+ const captura = ultimaCaptura(carpeta, nacimientoDeHilo.get(deHilo) ?? Date.now());
989
+ if (maqueta === undefined || captura === undefined)
990
+ return undefined;
991
+ try {
992
+ const comparacion = medirContraMaqueta(join(carpeta, captura), maqueta.disco);
993
+ ultimaMedidaDeHilo.set(hiloPadre, resumenDeMedida(comparacion, captura));
994
+ const ronda = (rondasVisualesDeHilo.get(hiloPadre) ?? 0) + 1;
995
+ rondasVisualesDeHilo.set(hiloPadre, ronda);
996
+ const escribio = ultimaEscrituraDeHilo.get(hiloPadre);
997
+ const critico = ultimaCriticaDeHilo.get(hiloPadre);
998
+ return textoDeMedidaAutomatica({
999
+ quien: quien.nombre,
1000
+ captura,
1001
+ maqueta: maqueta.virtual,
1002
+ comparacion,
1003
+ pedirCritica: critico === undefined || (escribio !== undefined && escribio > critico),
1004
+ ronda,
1005
+ });
1006
+ }
1007
+ catch (e) {
1008
+ anotarPaso("trueforge.medida", `no se pudo medir ${captura}: ${e instanceof Error ? e.message : String(e)}`)();
1009
+ return undefined;
1010
+ }
1011
+ }
629
1012
  async function* paso(lote, senal, pendientes, preguntas = []) {
630
1013
  for await (const _ of orquestador.send(lote))
631
1014
  void _;
@@ -648,13 +1031,22 @@ export async function abrirSesionTrueforge(opciones) {
648
1031
  args = {};
649
1032
  }
650
1033
  llamadas.set(claveDe(deHilo, t.id), { nombre: t.function.name, args });
1034
+ if (t.function.name === "write_file" || t.function.name === "edit_file")
1035
+ escriturasEnVuelo.set(claveDe(deHilo, t.id), { nombre: t.function.name, args });
1036
+ if (t.function.name === "xone_critica_visual")
1037
+ ultimaCriticaDeHilo.set(deHilo, Date.now());
651
1038
  // La traza: la MISMA lista blanca que el evento (`detalleDe`, `parametrosDe`), nunca
652
1039
  // los argumentos crudos.
653
1040
  const quien = quienEs.get(deHilo) ?? deHilo;
654
1041
  diagnostico?.herramienta(t.function.name, detalleDe(t.function.name, args), parametrosDe(t.function.name, args), tracker, deHilo === HILO_RAIZ ? "orquestador" : "especialista", undefined, quien);
1042
+ // Una CAPTURA más de quien ejecuta: es lo que dispara el aviso de capturas (`textoDeCapturas`).
1043
+ if (t.function.name === "execute" && typeof args["command"] === "string" && COMANDO_DE_CAPTURA.test(args["command"])) {
1044
+ capturasDeCadaHilo.set(deHilo, (capturasDeCadaHilo.get(deHilo) ?? 0) + 1);
1045
+ }
655
1046
  // Qué le pide el orquestador a quién: el encargo, acotado (ver `DiagnosticoDeTools.delegacion`).
656
1047
  if (t.function.name === "create_sub_agent" && typeof args["name"] === "string" && typeof args["input"] === "string") {
657
1048
  diagnostico?.delegacion?.(quien, args["name"], args["input"]);
1049
+ esperas.anunciar(args["name"]);
658
1050
  }
659
1051
  }
660
1052
  }
@@ -664,8 +1056,52 @@ export async function abrirSesionTrueforge(opciones) {
664
1056
  const llamada = llamadas.get(claveDe(deHilo, evento.tool_call_id));
665
1057
  const chars = typeof evento.content === "string" ? evento.content.length : 0;
666
1058
  diagnostico?.resultado?.(llamada?.nombre, llamada === undefined ? undefined : detalleDe(llamada.nombre, llamada.args), chars);
1059
+ // Lo que ESCRIBIÓ este hilo, para decírselo a quien lo llamó al terminar (`informesDeHijos.ts`).
1060
+ const escritura = escriturasEnVuelo.get(claveDe(deHilo, evento.tool_call_id));
1061
+ if (escritura !== undefined) {
1062
+ escriturasEnVuelo.delete(claveDe(deHilo, evento.tool_call_id));
1063
+ if (deHilo !== HILO_RAIZ && escrituraConExito(evento.content)) {
1064
+ const cambios = escritosDeCadaHilo.get(deHilo) ?? new Map();
1065
+ anotarEscritura(cambios, escritura.nombre, escritura.args);
1066
+ escritosDeCadaHilo.set(deHilo, cambios);
1067
+ ultimaEscrituraDeHilo.set(deHilo, Date.now());
1068
+ }
1069
+ }
667
1070
  }
668
1071
  // Un hilo que agotó su tope se ANOTA en la traza, con quién era: el raíz y cualquier hijo.
1072
+ if (evento.type === "internal.agent.done") {
1073
+ esperas.murio(deHilo);
1074
+ const hiloPadre = padreDeHilo.get(deHilo);
1075
+ // Lo que devolvió a quien lo llamó, a la traza (`DiagnosticoDeTools.devolucion`): la otra mitad del encargo.
1076
+ const devuelto = evento.send_to_parent?.content;
1077
+ if (hiloPadre !== undefined && devuelto !== undefined) {
1078
+ const texto = typeof devuelto === "string" ? devuelto : JSON.stringify(devuelto);
1079
+ diagnostico?.devolucion?.(quienEs.get(deHilo) ?? deHilo, quienEs.get(hiloPadre) ?? (hiloPadre === HILO_RAIZ ? "orquestador" : hiloPadre), texto);
1080
+ }
1081
+ if (hiloPadre !== undefined) {
1082
+ padreDeHilo.delete(deHilo);
1083
+ const nombre = especialistaDeHilo.get(deHilo);
1084
+ const quien = especialistas().find((a) => a.nombre === nombre);
1085
+ const medida = medidaTrasProbar(deHilo, hiloPadre, quien);
1086
+ const texto = medida ??
1087
+ textoDelInforme(quienEs.get(deHilo) ?? deHilo, escritosDeCadaHilo.get(deHilo) ?? new Map(), quien !== undefined && quien.motor === "modelo" && clasesDeTools(quien) === "escribe", ultimaMedidaDeHilo.get(deHilo));
1088
+ escritosDeCadaHilo.delete(deHilo);
1089
+ nacimientoDeHilo.delete(deHilo);
1090
+ if (texto !== undefined)
1091
+ informesPendientes.set(hiloPadre, [...(informesPendientes.get(hiloPadre) ?? []), texto]);
1092
+ }
1093
+ const h = hijosConMemoria.get(deHilo);
1094
+ if (h !== undefined) {
1095
+ hijosConMemoria.delete(deHilo);
1096
+ const foto = h.hilo.toSnapshot();
1097
+ const tokens = (foto.current_context_usage?.prompt_tokens ?? 0) + (foto.current_context_usage?.completion_tokens ?? 0);
1098
+ const destino = memoriaDeEspecialistas.cerrar(deHilo, {
1099
+ // sin que nadie se lo haya pedido, así que no deja memoria (como uno que falló).
1100
+ bien: evento.status !== "error" && !detencion.soloTexto(deHilo), mensajes: [...h.iniciales, ...foto.context], tokens
1101
+ });
1102
+ diagnostico?.memoria?.(h.nombre, "cierra", destino, tokens);
1103
+ }
1104
+ }
669
1105
  const tope = topeAgotadoDe(evento);
670
1106
  if (tope !== undefined)
671
1107
  diagnostico?.corte?.(quienEs.get(deHilo) ?? deHilo, tope);
@@ -745,6 +1181,7 @@ export async function abrirSesionTrueforge(opciones) {
745
1181
  const memoriaDescartada = avisoDeMemoria;
746
1182
  avisoDeMemoria = undefined;
747
1183
  let cortadoPorTope = false;
1184
+ let preguntasSolas = 0;
748
1185
  let sinResolver = 0;
749
1186
  // El veredicto del turno, con las MISMAS reglas que deepagents (`verificacion.ts`).
750
1187
  let veredicto = "no-corrio";
@@ -770,6 +1207,36 @@ export async function abrirSesionTrueforge(opciones) {
770
1207
  const preguntas = [];
771
1208
  yield* paso(lote, senal, pendientes, preguntas);
772
1209
  if (pendientes.length === 0) {
1210
+ /**
1211
+ * **En modo autónomo no se le pregunta a nadie**, y se dice en el chat. Una pregunta con una opción
1212
+ * `(Recommended)` se contesta con ella; una sin recomendada vuelve al agente pidiéndole que decida él y diga
1213
+ * qué eligió. Era «todas o ninguna»: medido en MyAllXOne, el orquestador hizo DOS a la vez —una con
1214
+ * recomendada, la otra no— y no se contestó ninguna, se enseñó solo la primera y la segunda se perdió. Con
1215
+ * tope, para que no dé vueltas.
1216
+ */
1217
+ if (preguntas.length > 0 && opciones.sinAprobacion?.() === true && preguntasSolas + preguntas.length <= TOPE_DE_PREGUNTAS_CONTESTADAS_SOLAS) {
1218
+ preguntasSolas += preguntas.length;
1219
+ const respuestas = [];
1220
+ for (const q of preguntas) {
1221
+ const { pregunta, opciones: posibles } = consultaDe(q.args);
1222
+ const elegida = opcionRecomendada(posibles);
1223
+ yield {
1224
+ tipo: "aviso",
1225
+ texto: elegida !== undefined
1226
+ ? `modo autónomo: a «${pregunta}» contesto la recomendada: ${elegida}`
1227
+ : `modo autónomo: «${pregunta}» no se pregunta; lo decide el agente y lo dirá`,
1228
+ severidad: "aviso",
1229
+ };
1230
+ respuestas.push({
1231
+ type: "user.tool_response",
1232
+ thread_id: q.hilo,
1233
+ tool_call_id: q.id,
1234
+ content: elegida ?? RESPUESTA_AUTONOMA_SIN_RECOMENDADA,
1235
+ });
1236
+ }
1237
+ lote = respuestas;
1238
+ continue;
1239
+ }
773
1240
  // El orquestador PREGUNTA: el turno acaba aquí con la pregunta a la vista, y lo que la
774
1241
  // persona escriba después vuelve como la respuesta de esa tool (`turno`, arriba).
775
1242
  const pregunta = preguntas[0];
@@ -1163,6 +1630,21 @@ export async function abrirSesionTrueforge(opciones) {
1163
1630
  },
1164
1631
  nuevoHilo(id) {
1165
1632
  hilo = id ?? `tf-${Date.now()}`;
1633
+ // Otra conversación: lo que los especialistas recordaban era de la anterior.
1634
+ memoriaDeEspecialistas.olvidar();
1635
+ esperas.olvidar();
1636
+ vueltasDeCadaHilo.clear();
1637
+ capturasDeCadaHilo.clear();
1638
+ escritosDeCadaHilo.clear();
1639
+ escriturasEnVuelo.clear();
1640
+ nacimientoDeHilo.clear();
1641
+ ultimaEscrituraDeHilo.clear();
1642
+ ultimaCriticaDeHilo.clear();
1643
+ ultimaMedidaDeHilo.clear();
1644
+ rondasVisualesDeHilo.clear();
1645
+ padreDeHilo.clear();
1646
+ informesPendientes.clear();
1647
+ hijosConMemoria.clear();
1166
1648
  // Un hilo NUEVO: lo que hubiera guardado con ese id no se pisa ni se carga a medias. Y una
1167
1649
  // pregunta de la conversación de antes no la contesta el primer mensaje de la nueva.
1168
1650
  const foto = fotoDeLaSesion(hilo);