xonecode 0.6.0 → 0.8.0

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 (128) hide show
  1. package/apps/web/dist/assets/{index-rqecK6s2.css → index-Boak51UP.css} +1 -1
  2. package/apps/web/dist/assets/index-Cg28x6nG.js +477 -0
  3. package/apps/web/dist/assets/{php-BBij3Lhc.js → php-Cu6F_QCJ.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-Dq2YEaLo.js → ruby-BiqQPoC6.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/cloudstudio/cloudstudioClient.js +10 -0
  7. package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -1
  8. package/dist/agent/cloudstudio/descarga.js +14 -4
  9. package/dist/agent/cloudstudio/descarga.js.map +1 -1
  10. package/dist/agent/cloudstudio/ramaActiva.js +11 -0
  11. package/dist/agent/cloudstudio/ramaActiva.js.map +1 -0
  12. package/dist/agent/cloudstudio/subida.js +13 -4
  13. package/dist/agent/cloudstudio/subida.js.map +1 -1
  14. package/dist/agent/config/catalogoModelos.js +45 -0
  15. package/dist/agent/config/catalogoModelos.js.map +1 -1
  16. package/dist/agent/config/modelos.js +103 -12
  17. package/dist/agent/config/modelos.js.map +1 -1
  18. package/dist/agent/config/mudanzaEnDisco.js +121 -0
  19. package/dist/agent/config/mudanzaEnDisco.js.map +1 -0
  20. package/dist/agent/config/selectorEnMaquina.js +45 -0
  21. package/dist/agent/config/selectorEnMaquina.js.map +1 -0
  22. package/dist/agent/config/settingsEnDisco.js +35 -0
  23. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  24. package/dist/agent/dispositivos/juezVisual.js +152 -0
  25. package/dist/agent/dispositivos/juezVisual.js.map +1 -0
  26. package/dist/agent/dispositivos/sensorDeArranque.js +154 -0
  27. package/dist/agent/dispositivos/sensorDeArranque.js.map +1 -0
  28. package/dist/agent/exportarPdf.js +140 -0
  29. package/dist/agent/exportarPdf.js.map +1 -0
  30. package/dist/agent/grafo/criticaVisual.js +115 -0
  31. package/dist/agent/grafo/criticaVisual.js.map +1 -0
  32. package/dist/agent/grafo/navegacionXone.js +37 -4
  33. package/dist/agent/grafo/navegacionXone.js.map +1 -1
  34. package/dist/agent/grafo/perfiles.js +52 -9
  35. package/dist/agent/grafo/perfiles.js.map +1 -1
  36. package/dist/agent/grafo/proyecto.js +74 -7
  37. package/dist/agent/grafo/proyecto.js.map +1 -1
  38. package/dist/agent/grafo/skills.js +32 -0
  39. package/dist/agent/grafo/skills.js.map +1 -1
  40. package/dist/agent/grafo/xoneAgent.js +103 -13
  41. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  42. package/dist/agent/navegacion/estilosEnDisco.js +110 -0
  43. package/dist/agent/navegacion/estilosEnDisco.js.map +1 -0
  44. package/dist/agent/navegacion/hechosEnDisco.js +26 -0
  45. package/dist/agent/navegacion/hechosEnDisco.js.map +1 -0
  46. package/dist/agent/navegacion/indiceEnDisco.js +15 -0
  47. package/dist/agent/navegacion/indiceEnDisco.js.map +1 -1
  48. package/dist/agent/subagentes/agentesEnDisco.js +85 -24
  49. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  50. package/dist/agent/subagentes/escrituraExterna.js +53 -0
  51. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  52. package/dist/agent/subagentes/pluginDeSkills.js +99 -0
  53. package/dist/agent/subagentes/pluginDeSkills.js.map +1 -0
  54. package/dist/agent/subagentes/subagenteExterno.js +48 -0
  55. package/dist/agent/subagentes/subagenteExterno.js.map +1 -1
  56. package/dist/agent/subagentes/subagenteOpencode.js +53 -1
  57. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -1
  58. package/dist/agent/tareas/juezDeTarea.js +1 -1
  59. package/dist/agent/tareas/juezDeTarea.js.map +1 -1
  60. package/dist/agent/tareas/tareasEnDisco.js +49 -0
  61. package/dist/agent/tareas/tareasEnDisco.js.map +1 -1
  62. package/dist/agent/turno/diagnosticoDeTools.js +5 -1
  63. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -1
  64. package/dist/agent/turno/informeDeTraza.js +27 -4
  65. package/dist/agent/turno/informeDeTraza.js.map +1 -1
  66. package/dist/agent/turno/puente.js +24 -1
  67. package/dist/agent/turno/puente.js.map +1 -1
  68. package/dist/agent/turno/resumenDeContexto.js +88 -4
  69. package/dist/agent/turno/resumenDeContexto.js.map +1 -1
  70. package/dist/agent/turno/resumenDeTool.js +18 -0
  71. package/dist/agent/turno/resumenDeTool.js.map +1 -1
  72. package/dist/agent/turno/rubrica.js +91 -0
  73. package/dist/agent/turno/rubrica.js.map +1 -0
  74. package/dist/agent/turno/turnoReal.js +172 -11
  75. package/dist/agent/turno/turnoReal.js.map +1 -1
  76. package/dist/cli/consola.js +63 -0
  77. package/dist/cli/consola.js.map +1 -1
  78. package/dist/cli/main.js +51 -4
  79. package/dist/cli/main.js.map +1 -1
  80. package/dist/cli/run.js +8 -0
  81. package/dist/cli/run.js.map +1 -1
  82. package/dist/core/agentes.js +111 -9
  83. package/dist/core/agentes.js.map +1 -1
  84. package/dist/core/esfuerzo.js +141 -0
  85. package/dist/core/esfuerzo.js.map +1 -0
  86. package/dist/core/estilos.js +78 -0
  87. package/dist/core/estilos.js.map +1 -0
  88. package/dist/core/exportacion.js +140 -0
  89. package/dist/core/exportacion.js.map +1 -0
  90. package/dist/core/hechosDelProyecto.js +87 -0
  91. package/dist/core/hechosDelProyecto.js.map +1 -0
  92. package/dist/core/logDeDispositivo.js +143 -0
  93. package/dist/core/logDeDispositivo.js.map +1 -0
  94. package/dist/core/modelos.js +32 -0
  95. package/dist/core/modelos.js.map +1 -1
  96. package/dist/core/mudanzaDeWorkspace.js +93 -0
  97. package/dist/core/mudanzaDeWorkspace.js.map +1 -0
  98. package/dist/core/planes.js +70 -0
  99. package/dist/core/planes.js.map +1 -0
  100. package/dist/core/ports.js +4 -0
  101. package/dist/core/ports.js.map +1 -1
  102. package/dist/core/rubrica.js +90 -0
  103. package/dist/core/rubrica.js.map +1 -0
  104. package/dist/core/selectorDeCarpeta.js +87 -0
  105. package/dist/core/selectorDeCarpeta.js.map +1 -0
  106. package/dist/core/settings.js +54 -5
  107. package/dist/core/settings.js.map +1 -1
  108. package/dist/core/shellDeAgente.js +45 -0
  109. package/dist/core/shellDeAgente.js.map +1 -1
  110. package/dist/core/topeAgotado.js +69 -0
  111. package/dist/core/topeAgotado.js.map +1 -0
  112. package/dist/web/servidor/arranque.js +324 -9
  113. package/dist/web/servidor/arranque.js.map +1 -1
  114. package/dist/web/servidor/sesiones.js +30 -2
  115. package/dist/web/servidor/sesiones.js.map +1 -1
  116. package/dist/web/servidor/transporte.js.map +1 -1
  117. package/dist/web/servidor/vestibulo.js +55 -6
  118. package/dist/web/servidor/vestibulo.js.map +1 -1
  119. package/package.json +2 -1
  120. package/skills/xone-hotswap/SKILL.md +31 -9
  121. package/skills/xone-hotswap/scripts/xone-arrancar-android +202 -0
  122. package/skills/xone-hotswap/scripts/xone-captura-android +93 -0
  123. package/skills/xone-hotswap/scripts/xone-desplegar-android +28 -0
  124. package/skills/xone-hotswap/scripts/xone-hotswap +39 -9
  125. package/skills/xone-hotswap/scripts/xone-reiniciar-android +28 -0
  126. package/skills/xone-plan-builder/SKILL.md +22 -4
  127. package/skills/xone-spec-builder/SKILL.md +37 -7
  128. package/apps/web/dist/assets/index-DBZTodio.js +0 -477
@@ -62,8 +62,10 @@ import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarModeloGlobal, }
62
62
  import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDisco.js";
63
63
  import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
64
64
  import { crearCheckpointerDeProyecto, hayCheckpoint, olvidarHilo, } from "../../agent/sesiones/checkpointer.js";
65
- import { cargarSettings, guardarConcurrenciaDeTareas, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, } from "../../agent/config/settingsEnDisco.js";
66
- import { dentroDelWorkspace, seAplicaSinAprobacion } from "../../core/settings.js";
65
+ import { cargarSettings, guardarConcurrenciaDeTareas, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, } from "../../agent/config/settingsEnDisco.js";
66
+ import { dentroDelWorkspace, expandirConCasa, motivoDeWorkspaceInaceptable, seAplicaSinAprobacion, } from "../../core/settings.js";
67
+ import { mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
68
+ import { elegirCarpetaEnMaquina, haySelectorDeCarpeta } from "../../agent/config/selectorEnMaquina.js";
67
69
  import { cloudstudioDelProyecto } from "../../agent/config/configEnDisco.js";
68
70
  import { abrirEnSistema } from "../../agent/cloudstudio/cloudstudioMcp.js";
69
71
  import { nombreDePersona } from "../../agent/config/persona.js";
@@ -73,7 +75,7 @@ import { marcarTareaDeSesion, sembrarConsumosPendientes } from "./sesiones.js";
73
75
  import { RUTA_ARTEFACTOS, esRutaDeArtefacto } from "../../core/artefactos.js";
74
76
  import { arbolDeProyecto, leerFicheroDeProyecto, motivoDeRutaInaceptable, } from "../../agent/grafo/arbolDeProyecto.js";
75
77
  import { leerArtefactoCrudo, leerArtefactoDeSesion, } from "../../agent/grafo/artefactosEnDisco.js";
76
- import { CatalogoModelos } from "../../agent/config/catalogoModelos.js";
78
+ import { CatalogoModelos, baseUrlDeOllama, baseUrlDeOllamaCloud, capacidadesDeOllama, } from "../../agent/config/catalogoModelos.js";
77
79
  import { Modelos } from "../../agent/config/modelos.js";
78
80
  import { crearJuezDeTarea, invocarConModelos } from "../../agent/tareas/juezDeTarea.js";
79
81
  import { SIN_CONSUMO } from "../../agent/subagentes/consumoExterno.js";
@@ -85,13 +87,14 @@ import { aplicarFeedback, TOPE_DE_ADJUNTO } from "../../agent/tareas/tareasEnDis
85
87
  import { nombreDeAdjuntoAceptable } from "../../core/adjuntos.js";
86
88
  import { rutaMemoriaDeProyecto } from "../../agent/grafo/memoriaDeProyecto.js";
87
89
  import { crearAumentador, invocarParaAumentar } from "../../agent/tareas/aumentador.js";
88
- import { baseDeWorkspacePorOmision, conexionDeVestibulo, crearVestibulo, escribirProyectoEnDisco, esProyectoEnDisco, } from "./vestibulo.js";
90
+ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo, crearVestibulo, escribirProyectoEnDisco, esProyectoEnDisco, } from "./vestibulo.js";
89
91
  // Valor y no tipo: la traducción de una `Tarea` a lo que viaja vive JUNTO al tipo que
90
92
  // produce y no aquí. Estuvo en este cierre, y ahí se cayó `veredicto` sin que nada se
91
93
  // pusiera rojo (F1 de la revisión final) — el patrón de fallo que este repo ya tiene
92
94
  // documentado cinco veces: una composición de producción dentro de algo que los tests
93
95
  // doblan.
94
96
  import { filaDeTarea } from "./transporte.js";
97
+ import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
95
98
  /** Las dos rutas del cable. El cliente las tiene escritas en `apps/web/src/conexion.ts`. */
96
99
  /**
97
100
  * Cuánto se espera al aviso de «lo que ya había sin commitear» antes de anunciar sin él.
@@ -486,6 +489,18 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
486
489
  * recuerda —como fallo— hasta que alguien lo vuelva a pedir a propósito.
487
490
  */
488
491
  const catalogos = new Map();
492
+ /**
493
+ * Lo que el servidor de Ollama ha dicho de cada modelo, por `proveedor/modelo`.
494
+ *
495
+ * Es una memoria y no una caché con plazo: se llena la primera vez que ese modelo está en
496
+ * vigor y se vuelve a emitir el estado cuando llega. Ollama es el único que necesita esto
497
+ * —los demás salen de la tabla, que es síncrona—, y lo necesita porque sus modelos los
498
+ * elige el usuario: no hay prefijo que diga si `ministral-3:3b` piensa, pero `/api/show`
499
+ * sí lo contesta.
500
+ */
501
+ const capacidadesVivas = new Map();
502
+ /** Las que ya se están preguntando, para no lanzar una consulta por cliente conectado. */
503
+ const capacidadesEnVuelo = new Set();
489
504
  /**
490
505
  * El estado de modelos: qué está en vigor y qué se puede elegir.
491
506
  *
@@ -493,7 +508,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
493
508
  * proyecto abierto no hay sesión y por tanto no hay modelo que afirmar: el campo se va
494
509
  * y el cliente pinta «Elige modelo» en vez de una fila muerta.
495
510
  */
496
- const emitirModelos = () => emitir(mensajeDeModelos());
511
+ const emitirModelos = () => {
512
+ emitir(mensajeDeModelos());
513
+ // Después de emitir, no antes: lo que ya se sabe se pinta ya, y lo que falta llega en
514
+ // una segunda emisión. Al revés, cada cambio de modelo dejaría la pastilla esperando
515
+ // una consulta de red para pintar lo que no depende de ella.
516
+ asegurarCapacidades();
517
+ };
497
518
  /**
498
519
  * Los subagentes en vigor, compuestos pero sin mandar.
499
520
  *
@@ -516,6 +537,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
516
537
  // Ausente = no la tiene. No se emite `false` para que el cliente no tenga que
517
538
  // distinguir entre «no» y «no consta»: aquí son lo mismo y la ausencia lo dice.
518
539
  ...(a.ejecucion === true ? { ejecucion: true } : {}),
540
+ // Igual que arriba: ausente es «no hay ninguno fijado», que es un dato y no un hueco.
541
+ ...(a.esfuerzo === undefined ? {} : { esfuerzo: a.esfuerzo }),
519
542
  skills: a.skills,
520
543
  instrucciones: a.instrucciones,
521
544
  origen: a.origen,
@@ -781,10 +804,43 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
781
804
  // pueda leer el config global. Sin el segundo, Ajustes enseñaría «sin elegir» sobre una
782
805
  // máquina con un defecto escrito, justo en la pantalla donde se configura.
783
806
  const porDefecto = opciones.modeloPorDefecto?.();
807
+ /**
808
+ * El esfuerzo del modelo EN VIGOR, que es lo único de lo que se puede afirmar algo.
809
+ *
810
+ * Sin sesión no hay modelo en vigor y no viaja: la pastilla no se pinta, igual que
811
+ * `actual` no se inventa. Los niveles salen de la tabla pura; para Ollama hacen falta
812
+ * las capacidades vivas, y mientras no hayan llegado tampoco se afirma nada — se
813
+ * piden aparte y el estado se vuelve a emitir cuando contesten.
814
+ */
815
+ const niveles = trabajo === undefined
816
+ ? undefined
817
+ : nivelesDeEsfuerzo(trabajo.proveedor, trabajo.modelo, capacidadesVivas.get(`${trabajo.proveedor}/${trabajo.modelo}`));
818
+ const elegido = abierto?.estadoDeSesion.esfuerzo;
784
819
  return {
785
820
  clase: "modelos",
786
821
  ...(trabajo === undefined ? {} : { actual: `${trabajo.proveedor}/${trabajo.modelo}` }),
787
822
  ...(porDefecto === undefined ? {} : { porDefecto }),
823
+ ...(niveles === undefined
824
+ ? {}
825
+ : {
826
+ esfuerzo: {
827
+ niveles: [...niveles],
828
+ // El nivel guardado solo viaja si ESTE modelo lo admite. Si no, la pastilla
829
+ // diría «xhigh» sobre un modelo que va a ignorarlo — el valor sigue en la
830
+ // sesión y vuelve solo al cambiar a un modelo que lo acepte.
831
+ ...(elegido !== undefined && niveles.includes(elegido) ? { actual: elegido } : {}),
832
+ // La nota de Ollama, MEDIDA: su servidor valida los cuatro niveles, pero el
833
+ // efecto lo pone el template de cada modelo y no es monótono —en
834
+ // `glm-5.3-flash:cloud`, `high` mide menos razonamiento que `medium`—.
835
+ // Callarlo haría que la pastilla afirmara una escala que ahí no existe.
836
+ ...(trabajo?.proveedor === "ollama" || trabajo?.proveedor === "ollama-cloud"
837
+ ? {
838
+ nota: "En Ollama el efecto de cada nivel lo decide el modelo, no el servidor:"
839
+ + " medido, en algunos «high» razona MENOS que «medium».",
840
+ }
841
+ : {}),
842
+ },
843
+ }),
788
844
  proveedores: [...PROVEEDORES, ...idsPersonalizados()].map((p) => ({
789
845
  id: p,
790
846
  nombre: nombreDeProveedor(p, personalizados()),
@@ -845,6 +901,20 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
845
901
  for (const cliente of destinatarios)
846
902
  actos = destino.conectar(cliente);
847
903
  const modelos = mensajeDeModelos();
904
+ /**
905
+ * Y se pregunta por lo que falte, que en esta rama hay que pedir A MANO.
906
+ *
907
+ * La ráfaga COMPONE el mensaje en vez de emitirlo —va solo al recién llegado—, así que
908
+ * no pasa por `emitirModelos` y sin esta línea el camino más normal de todos —abrir el
909
+ * navegador sobre un proyecto ya abierto— no preguntaba nunca: la pastilla de esfuerzo
910
+ * no aparecía con Ollama hasta que algo más provocara una emisión. Lo encontró el test
911
+ * del cable, que es exactamente el patrón de fallo de esta arquitectura.
912
+ *
913
+ * No se mete DENTRO de `mensajeDeModelos` porque eso es un compositor: un efecto de red
914
+ * escondido en la función que arma un mensaje es la clase de cosa que luego dispara
915
+ * desde sitios que nadie esperaba.
916
+ */
917
+ asegurarCapacidades();
848
918
  // Los subagentes van en la ráfaga por lo mismo que los modelos: la ventana de ajustes
849
919
  // se puede abrir en cuanto conecta, y sin esto enseñaría una lista vacía hasta que algo
850
920
  // los cambiara — que es indistinguible de «no tienes ninguno».
@@ -856,6 +926,9 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
856
926
  // La cola de tareas, si esta ejecución las tiene: la misma regla que `agentes`, sin
857
927
  // esto la pestaña de tareas se quedaría vacía hasta el primer cambio de la cola.
858
928
  const tareas = mensajeDeTareas();
929
+ // El workspace, por lo mismo que los subagentes y las skills: Ajustes se puede abrir en
930
+ // cuanto conecta, y un campo en blanco se lee como «no hay ninguno puesto».
931
+ const workspace = mensajeDeWorkspace();
859
932
  /**
860
933
  * Lo consumido por la sesión que se va a pintar. Va en la ráfaga por lo mismo que los
861
934
  * modelos: una pestaña que conecta a mitad de sesión no vio los cambios anteriores, y
@@ -873,6 +946,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
873
946
  cliente(skills);
874
947
  if (tareas !== undefined)
875
948
  cliente(tareas);
949
+ if (workspace !== undefined)
950
+ cliente(workspace);
876
951
  if (consumoDeLaSesion !== undefined) {
877
952
  cliente({
878
953
  clase: "consumo",
@@ -1011,6 +1086,21 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1011
1086
  });
1012
1087
  /** Los cuatro nombres conocidos y nada más: lo que llega por el cable no elige binario. */
1013
1088
  const esNombreDeHerramienta = (v) => v === "adb" || v === "emulator" || v === "xcrun" || v === "devicectl";
1089
+ const mensajeDeWorkspace = () => {
1090
+ const ruta = opciones.workspace?.();
1091
+ if (ruta === undefined)
1092
+ return undefined;
1093
+ // `puedeElegir` va solo cuando es cierto: ausente ya significa que no, y un `false`
1094
+ // escrito solo daría dos formas de decir lo mismo.
1095
+ return opciones.elegirCarpeta === undefined
1096
+ ? { clase: "workspace", ruta }
1097
+ : { clase: "workspace", ruta, puedeElegir: true };
1098
+ };
1099
+ const emitirWorkspace = () => {
1100
+ const m = mensajeDeWorkspace();
1101
+ if (m !== undefined)
1102
+ emitir(m);
1103
+ };
1014
1104
  const mensajeDeTareas = () => {
1015
1105
  if (opciones.colaDeTareas === undefined)
1016
1106
  return undefined;
@@ -1450,7 +1540,28 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1450
1540
  }
1451
1541
  try {
1452
1542
  const modelos = await opciones.catalogoDeModelos(id);
1453
- catalogos.set(id, { modelos: modelos.map((m) => ({ id: m.id, ...(m.nombre === undefined ? {} : { nombre: m.nombre }) })) });
1543
+ /**
1544
+ * Cada modelo viaja con los niveles de esfuerzo que ADMITE, y se calculan aquí.
1545
+ *
1546
+ * Aquí y no en el cliente porque la tabla vive en `core/esfuerzo.ts` y la frontera
1547
+ * prohíbe que `apps/web/` importe de `src/`. La alternativa era una copia DECLARADA
1548
+ * al otro lado —como la del slug o la del workspace—, y no compensa: aquéllas son una
1549
+ * regla de tres líneas, y ésta es una tabla por familia que además cambia cada vez que
1550
+ * alguien MIDE un modelo nuevo. Dos tablas es un sitio donde una queda vieja sin que
1551
+ * nada lo diga.
1552
+ *
1553
+ * Ausente = ese modelo no admite ninguno, y entonces el control no se pinta.
1554
+ */
1555
+ catalogos.set(id, {
1556
+ modelos: modelos.map((m) => {
1557
+ const niveles = nivelesDeEsfuerzo(id, m.id);
1558
+ return {
1559
+ id: m.id,
1560
+ ...(m.nombre === undefined ? {} : { nombre: m.nombre }),
1561
+ ...(niveles === undefined ? {} : { esfuerzos: [...niveles] }),
1562
+ };
1563
+ }),
1564
+ });
1454
1565
  }
1455
1566
  catch (error) {
1456
1567
  // `ErrorCatalogoModelos` es publicable por contrato: nunca lleva la clave ni el
@@ -1876,6 +1987,72 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1876
1987
  * Si no hay escritor, se DICE que no se ha guardado en vez de callarlo: un ajuste que
1877
1988
  * parece puesto y no lo está es peor que uno que falta.
1878
1989
  */
1990
+ /**
1991
+ * El esfuerzo de la sesión abierta. `nivel` ausente = quitarlo.
1992
+ *
1993
+ * Se encola `/esfuerzo`, que es el MISMO manejador que usa el terminal: la función se
1994
+ * comparte y la sintaxis no se exporta, igual que con `/modelo`. Y **no se guarda ningún
1995
+ * defecto**: a diferencia del modelo, esto es de la sesión y solo de la sesión — un
1996
+ * defecto persistido es otra decisión y no se toma por inercia.
1997
+ *
1998
+ * Sin proyecto abierto no hay sesión a la que aplicárselo, y se DICE en vez de callarlo:
1999
+ * la pastilla solo se pinta con sesión, así que llegar aquí sin ella es un cliente
2000
+ * desincronizado y merece una frase, no un silencio.
2001
+ */
2002
+ const atenderEsfuerzo = (nivel) => {
2003
+ const abierto = vestibulo.proyectoAbierto();
2004
+ if (abierto === undefined) {
2005
+ informar("no hay ninguna sesión abierta a la que fijarle el esfuerzo");
2006
+ return;
2007
+ }
2008
+ if (nivel === undefined) {
2009
+ abierto.consola.encolar("/esfuerzo ninguno");
2010
+ return;
2011
+ }
2012
+ // Se criba contra el vocabulario ANTES de encolar: lo que llega del cable acaba siendo
2013
+ // un parámetro de una API, y el manejador ya lo rechazaría, pero su rechazo se
2014
+ // imprimiría como una línea de consola en vez de un aviso. Aquí se dice mejor.
2015
+ if (!esEsfuerzo(nivel)) {
2016
+ informar(`«${nivel}» no es un nivel de esfuerzo`);
2017
+ return;
2018
+ }
2019
+ abierto.consola.encolar(`/esfuerzo ${nivel}`);
2020
+ };
2021
+ /**
2022
+ * Pregunta a Ollama si el modelo en vigor piensa, y vuelve a emitir cuando conteste.
2023
+ *
2024
+ * Una vez por modelo y por proceso: la respuesta se guarda en `capacidadesVivas`, y sin
2025
+ * el conjunto `capacidadesEnVuelo` cada cliente que se conecta lanzaría la suya. Mismo
2026
+ * molde que `catalogos`.
2027
+ *
2028
+ * Solo Ollama, porque solo Ollama lo necesita: los demás salen de la tabla, que es pura
2029
+ * y síncrona. Y su fallo se TRAGA — lo que no se pudo preguntar deja la pastilla sin
2030
+ * pintar, que es la dirección segura y ya es lo que pasaba antes de que esto existiera.
2031
+ */
2032
+ const asegurarCapacidades = () => {
2033
+ if (opciones.capacidadesDeModelo === undefined)
2034
+ return;
2035
+ const abierto = vestibulo.proyectoAbierto();
2036
+ if (abierto === undefined)
2037
+ return;
2038
+ const trabajo = resolver(abierto.estadoDeSesion.fuentes).trabajo;
2039
+ if (trabajo.proveedor !== "ollama" && trabajo.proveedor !== "ollama-cloud")
2040
+ return;
2041
+ const clave = `${trabajo.proveedor}/${trabajo.modelo}`;
2042
+ if (capacidadesVivas.has(clave) || capacidadesEnVuelo.has(clave))
2043
+ return;
2044
+ capacidadesEnVuelo.add(clave);
2045
+ void opciones
2046
+ .capacidadesDeModelo(trabajo.proveedor, trabajo.modelo)
2047
+ .then((vivas) => {
2048
+ if (vivas !== undefined) {
2049
+ capacidadesVivas.set(clave, vivas);
2050
+ emitirModelos();
2051
+ }
2052
+ })
2053
+ .catch(() => { })
2054
+ .finally(() => capacidadesEnVuelo.delete(clave));
2055
+ };
1879
2056
  const atenderModelo = (id) => {
1880
2057
  try {
1881
2058
  parsear(id);
@@ -3329,6 +3506,40 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3329
3506
  respuesta.end();
3330
3507
  return;
3331
3508
  }
3509
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "workspace" && typeof mensaje.ruta === "string") {
3510
+ /**
3511
+ * La regla se aplica AQUÍ además de en la pantalla. El cliente lleva su copia
3512
+ * declarada para poder explicar el no —`informar` no llega al navegador desde el
3513
+ * vestíbulo—, pero la barrera vive en el servidor: una pantalla solo esconde un botón.
3514
+ *
3515
+ * Y se reemite SIEMPRE, se haya escrito o no: con el valor nuevo cuando valió y con el
3516
+ * de antes cuando no, así el campo acaba enseñando lo que hay en disco y no lo que se
3517
+ * tecleó. Un rechazo mudo dejaría la pantalla afirmando un ajuste que no está puesto.
3518
+ */
3519
+ opciones.guardarWorkspace?.(mensaje.ruta);
3520
+ emitirWorkspace();
3521
+ respuesta.writeHead(204);
3522
+ respuesta.end();
3523
+ return;
3524
+ }
3525
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "elegirCarpeta") {
3526
+ /**
3527
+ * El diálogo lo abre el SISTEMA donde corre la consola, así que esto tarda lo que
3528
+ * tarde una persona. Se contesta 204 en el acto y la carpeta llega después por el SSE:
3529
+ * dejar la petición abierta minutos enteros es lo que este servidor no hace en ningún
3530
+ * otro sitio.
3531
+ *
3532
+ * Se emite SIEMPRE, también sin carpeta: el cliente encendió su «abriendo…» al pedirlo
3533
+ * y sin respuesta se quedaría encendido para siempre después de un simple «cancelar».
3534
+ */
3535
+ void (async () => {
3536
+ const elegida = await opciones.elegirCarpeta?.(opciones.workspace?.());
3537
+ emitir(elegida === undefined ? { clase: "carpetaElegida" } : { clase: "carpetaElegida", ruta: elegida });
3538
+ })().catch(contar);
3539
+ respuesta.writeHead(204);
3540
+ respuesta.end();
3541
+ return;
3542
+ }
3332
3543
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "modelosDeMotor" && typeof mensaje.motor === "string") {
3333
3544
  void atenderModelosDeMotor(mensaje.motor).catch(contar);
3334
3545
  respuesta.writeHead(204);
@@ -3366,6 +3577,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3366
3577
  respuesta.end();
3367
3578
  return;
3368
3579
  }
3580
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "esfuerzo") {
3581
+ atenderEsfuerzo(mensaje.nivel);
3582
+ respuesta.writeHead(204);
3583
+ respuesta.end();
3584
+ return;
3585
+ }
3369
3586
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "agente") {
3370
3587
  atenderAgente(mensaje);
3371
3588
  respuesta.writeHead(204);
@@ -3841,12 +4058,71 @@ export function augmentacionCableada(opciones) {
3841
4058
  export function commitDeTurnoCableado(opciones) {
3842
4059
  const commitear = opciones.commitear ?? commitDeTurno;
3843
4060
  return async (raiz, mensaje, sesion) => {
3844
- if (!dentroDelWorkspace(raiz, opciones.base))
4061
+ if (!dentroDelWorkspace(raiz, opciones.base()))
3845
4062
  return undefined;
3846
4063
  const hecho = await commitear(raiz, mensaje, sesion);
3847
4064
  return hecho.via === "fallo" ? `no se pudo commitear el turno: ${hecho.motivo}` : undefined;
3848
4065
  };
3849
4066
  }
4067
+ /**
4068
+ * La mudanza de una vez de las copias que siguen en el reparto viejo, cableada — y extraída
4069
+ * por el motivo de siempre: compuesta dentro del cierre de `arrancarConsolaWeb`, que todos
4070
+ * sus tests doblan, la regla estaría escrita y no probada.
4071
+ *
4072
+ * Lo que aquí se puede caer sin que TypeScript diga nada son las DOS bases, que no son la
4073
+ * misma cuenta: la VIEJA es `settings.workspace` si estaba puesto —el reparto viejo metía su
4074
+ * `workspace/` en medio fuera cual fuera la base— o `~/.xonecode` a secas; la de AHORA es ese
4075
+ * mismo `settings.workspace` o `~/.xonecode/workspace`. Confundirlas en cualquiera de los dos
4076
+ * sentidos da el mismo resultado: una mudanza que no encuentra nada y no dice nada.
4077
+ *
4078
+ * Y se ESCRIBE en el terminal, nunca por el cable: lo que se cuenta son rutas de la máquina
4079
+ * y nombres de proyecto (`sinRutas`). Corre antes de que exista el vestíbulo, así que ni
4080
+ * siquiera hay cable donde emitirlo.
4081
+ */
4082
+ export function mudarWorkspaceLegadoCableado(opciones) {
4083
+ const mudar = opciones.mudar ?? mudarWorkspaceLegado;
4084
+ return mudar({
4085
+ legado: opciones.settings.workspace ?? baseDeWorkspaceLegada(),
4086
+ workspace: opciones.settings.workspace ?? baseDeWorkspacePorOmision(),
4087
+ entornos: opciones.settings.entornos.map((e) => e.id),
4088
+ escribir: opciones.escribir,
4089
+ });
4090
+ }
4091
+ /**
4092
+ * Las dos mitades del ajuste del workspace, cableadas — y extraídas por el motivo de
4093
+ * siempre: dentro del cierre de `arrancarConsolaWeb`, que todos sus tests doblan, ni una ni
4094
+ * otra estaría probada. Y aquí `guardarWorkspace` LLEVABA la marca: existía en
4095
+ * `agent/config/settingsEnDisco.ts` con un test propio y CERO llamadores de producción, o
4096
+ * sea el ajuste escrito que no hacía nada porque nadie lo había enchufado.
4097
+ *
4098
+ * Tres cosas que se deciden aquí y no en la pantalla:
4099
+ * - **Qué se LEE**: el disco, en cada emisión, no un valor capturado al arrancar. Es la misma
4100
+ * regla que `baseDeWorkspace`, y aquí además cierra el lazo: lo que se acaba de guardar es
4101
+ * lo que se vuelve a emitir.
4102
+ * - **Qué se ACEPTA**: `motivoDeWorkspaceInaceptable` sobre la ruta ya expandida, en el
4103
+ * servidor. El cliente lleva su copia declarada para explicar el no, pero una pantalla
4104
+ * solo esconde un botón.
4105
+ * - **Qué se GUARDA y qué se ENSEÑA**: la ruta ENTERA, nunca un `~/…`. En el fichero porque
4106
+ * la casa cambia de una máquina a otra y un `~` dentro solo significa algo para quien lo
4107
+ * escribió; en la pantalla porque una ruta abreviada no se puede comprobar de un vistazo,
4108
+ * que es para lo que ese campo existe. Se probó a mandarla abreviada —para que el caso
4109
+ * normal no llevara el nombre de la cuenta— y no se sostiene: la cabecera de esa misma
4110
+ * consola ya saluda por el nombre del usuario. El `~` sigue ACEPTÁNDOSE al teclear, que es
4111
+ * comodidad de entrada y otra cosa.
4112
+ */
4113
+ export function ajusteDeWorkspaceCableado(opciones) {
4114
+ const leer = opciones.leer ?? (() => cargarSettings().settings.workspace);
4115
+ const guardar = opciones.guardar ?? ((ruta) => void guardarWorkspaceEnDisco(undefined, ruta));
4116
+ return {
4117
+ workspace: () => leer() ?? baseDeWorkspacePorOmision(),
4118
+ guardarWorkspace: (ruta) => {
4119
+ const absoluta = expandirConCasa(ruta, opciones.casa);
4120
+ if (motivoDeWorkspaceInaceptable(absoluta) !== undefined)
4121
+ return;
4122
+ guardar(absoluta);
4123
+ },
4124
+ };
4125
+ }
3850
4126
  export function construirCorredorDeTareasCableado(opciones) {
3851
4127
  const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
3852
4128
  /**
@@ -4040,6 +4316,17 @@ export async function arrancarConsolaWeb(opciones) {
4040
4316
  if (offline && opciones.guion !== true) {
4041
4317
  escribir("este directorio es un proyecto offline: ábrelo con «xonecode --cli»\n");
4042
4318
  }
4319
+ /**
4320
+ * La mudanza del reparto viejo, ANTES de que exista el vestíbulo y de que arranque el
4321
+ * corredor de tareas. El orden no es estético: el vestíbulo compone raíces con el reparto
4322
+ * de AHORA y una tarea pendiente lleva su raíz grabada, así que cualquiera de los dos
4323
+ * corriendo antes abriría una carpeta que esta función está a punto de mover.
4324
+ *
4325
+ * Y va aquí y no en el vestíbulo porque no es del vestíbulo: es de la MÁQUINA, pasa una
4326
+ * sola vez, y cuando no quede nadie con el reparto viejo en disco se retira de un tirón
4327
+ * junto con `core/mudanzaDeWorkspace.ts`.
4328
+ */
4329
+ mudarWorkspaceLegadoCableado({ settings: cargarSettings().settings, escribir });
4043
4330
  const arrancar = opciones.crearServidor ?? arrancarServidor;
4044
4331
  const servidor = await arrancar({
4045
4332
  puerto: opciones.puerto,
@@ -4143,6 +4430,15 @@ export async function arrancarConsolaWeb(opciones) {
4143
4430
  // la raíz que la tarea necesita para poder correr. Ver el comentario de
4144
4431
  // `OpcionesDeMontaje.colaDeTareas`.
4145
4432
  ...opcionesDeTareas,
4433
+ // Dónde se bajan las copias: leerlo y elegirlo. La composición está EXTRAÍDA y probada
4434
+ // (`ajusteDeWorkspaceCableado`) por el patrón de fallo de siempre — y aquí el escritor
4435
+ // ya venía con la marca puesta: existía con su test y sin un solo llamador.
4436
+ ...ajusteDeWorkspaceCableado({ casa: homedir() }),
4437
+ // El selector nativo, solo si este sistema tiene uno. En el que no, la opción no se
4438
+ // monta y el botón no llega a existir.
4439
+ ...(haySelectorDeCarpeta()
4440
+ ? { elegirCarpeta: (desde) => elegirCarpetaEnMaquina(desde === undefined ? {} : { desde }) }
4441
+ : {}),
4146
4442
  // Los dos puertos del selector de modelos, con las piezas reales: quién tiene
4147
4443
  // credencial (`auth.json` o el entorno, leído desde el cwd) y el catálogo VIVO.
4148
4444
  hayCredencial: (proveedor) => hayCredencial(proveedor, opciones.cwd),
@@ -4227,6 +4523,14 @@ export async function arrancarConsolaWeb(opciones) {
4227
4523
  const modelos = await new CatalogoModelos(undefined, undefined, proveedoresPersonalizados).listar(proveedor);
4228
4524
  return modelos.map((m) => ({ id: m.id, ...(m.nombre === undefined ? {} : { nombre: m.nombre }) }));
4229
4525
  },
4526
+ /**
4527
+ * Solo Ollama tiene a quién preguntarle esto, y por eso los demás devuelven «no consta»
4528
+ * aquí en vez de que lo decida `asegurarCapacidades`: quien sabe qué proveedores saben
4529
+ * contestar es este cableado, no el servidor.
4530
+ */
4531
+ capacidadesDeModelo: async (proveedor, modelo) => proveedor === "ollama" || proveedor === "ollama-cloud"
4532
+ ? capacidadesDeOllama(modelo, proveedor === "ollama" ? baseUrlDeOllama() : baseUrlDeOllamaCloud())
4533
+ : undefined,
4230
4534
  // Se releen de disco en cada consulta, igual que los ajustes de dispositivos: esta
4231
4535
  // misma ventana los da de alta, y una lista capturada al montar las rutas se quedaría
4232
4536
  // vieja hasta reiniciar el proceso.
@@ -4393,14 +4697,25 @@ function vestibuloReal(opciones, servidor, escribir, informar) {
4393
4697
  // El commit del turno, con el sello de la sesión. La composición está EXTRAÍDA y probada
4394
4698
  // (`commitDeTurnoCableado`): el argumento que se cae en una lambda escrita a mano es
4395
4699
  // justo el que sostiene la atribución de Revisión.
4396
- commitearTurno: commitDeTurnoCableado({ base: settings.workspace ?? baseDeWorkspacePorOmision() }),
4700
+ commitearTurno: commitDeTurnoCableado({
4701
+ base: () => cargarSettings().settings.workspace ?? baseDeWorkspacePorOmision(),
4702
+ }),
4397
4703
  olvidarMarcaDeSesion: olvidarSesion,
4398
4704
  // La memoria del agente por hilo. `historica` deja de ser «se reabrió» para ser «no hay
4399
4705
  // checkpoint que cargar», y borrar una sesión se lleva también su checkpoint.
4400
4706
  hayMemoriaDeHilo: async (raiz, hilo) => hayCheckpoint(crearCheckpointerDeProyecto(raiz), hilo),
4401
4707
  olvidarMemoriaDeHilo: async (raiz, hilo) => olvidarHilo(crearCheckpointerDeProyecto(raiz), hilo),
4402
4708
  entornos: settings.entornos,
4403
- ...(settings.workspace === undefined ? {} : { baseDeWorkspace: settings.workspace }),
4709
+ /**
4710
+ * Dónde se bajan las copias, RELEÍDO en cada uso y no resuelto aquí.
4711
+ *
4712
+ * `settings.workspace` se cambia desde Ajustes con la consola en marcha, así que un
4713
+ * valor capturado al arrancar dejaría el resto del proceso bajando al sitio de antes
4714
+ * mientras la pantalla enseña el nuevo — el ajuste escrito que no hace nada. Es lo
4715
+ * mismo que ya hacen `sinAprobacion` y el tope de concurrencia, y por lo mismo: releer
4716
+ * un JSON pequeño no cuesta nada al lado de equivocarse de carpeta.
4717
+ */
4718
+ baseDeWorkspace: () => cargarSettings().settings.workspace ?? baseDeWorkspacePorOmision(),
4404
4719
  // La URL de la web para que la página del callback devuelva AQUÍ y no diga «vuelve a
4405
4720
  // la terminal», que en un navegador es falso.
4406
4721
  ...conexionDeVestibulo(servidor.url),