xonecode 0.5.1 → 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 (315) hide show
  1. package/apps/web/dist/assets/{index-BB3SamhR.css → index-Boak51UP.css} +1 -1
  2. package/apps/web/dist/assets/index-Cg28x6nG.js +477 -0
  3. package/apps/web/dist/assets/{php-D_2--9WK.js → php-Cu6F_QCJ.js} +1 -1
  4. package/apps/web/dist/assets/{ruby-DSHSDbk1.js → ruby-BiqQPoC6.js} +1 -1
  5. package/apps/web/dist/index.html +2 -2
  6. package/dist/agent/{cloudstudioClient.js → cloudstudio/cloudstudioClient.js} +10 -0
  7. package/dist/agent/cloudstudio/cloudstudioClient.js.map +1 -0
  8. package/dist/agent/{cloudstudioMcp.js → cloudstudio/cloudstudioMcp.js} +5 -5
  9. package/dist/agent/cloudstudio/cloudstudioMcp.js.map +1 -0
  10. package/dist/agent/{descarga.js → cloudstudio/descarga.js} +17 -7
  11. package/dist/agent/cloudstudio/descarga.js.map +1 -0
  12. package/dist/agent/cloudstudio/manifiesto.js.map +1 -0
  13. package/dist/agent/cloudstudio/ramaActiva.js +11 -0
  14. package/dist/agent/cloudstudio/ramaActiva.js.map +1 -0
  15. package/dist/agent/{subida.js → cloudstudio/subida.js} +16 -7
  16. package/dist/agent/cloudstudio/subida.js.map +1 -0
  17. package/dist/agent/{zip.js → cloudstudio/zip.js} +1 -1
  18. package/dist/agent/cloudstudio/zip.js.map +1 -0
  19. package/dist/agent/{authEnDisco.js → config/authEnDisco.js} +3 -3
  20. package/dist/agent/config/authEnDisco.js.map +1 -0
  21. package/dist/agent/{catalogoModelos.js → config/catalogoModelos.js} +47 -1
  22. package/dist/agent/config/catalogoModelos.js.map +1 -0
  23. package/dist/agent/{configEnDisco.js → config/configEnDisco.js} +6 -6
  24. package/dist/agent/config/configEnDisco.js.map +1 -0
  25. package/dist/agent/{crearProyecto.js → config/crearProyecto.js} +2 -2
  26. package/dist/agent/config/crearProyecto.js.map +1 -0
  27. package/dist/agent/config/entorno.js.map +1 -0
  28. package/dist/agent/config/gemini.js.map +1 -0
  29. package/dist/agent/config/modelos.js +235 -0
  30. package/dist/agent/config/modelos.js.map +1 -0
  31. package/dist/agent/{modelosDeMotor.js → config/modelosDeMotor.js} +1 -1
  32. package/dist/agent/config/modelosDeMotor.js.map +1 -0
  33. package/dist/agent/config/mudanzaEnDisco.js +121 -0
  34. package/dist/agent/config/mudanzaEnDisco.js.map +1 -0
  35. package/dist/agent/{persona.js → config/persona.js} +1 -1
  36. package/dist/agent/config/persona.js.map +1 -0
  37. package/dist/agent/config/selectorEnMaquina.js +45 -0
  38. package/dist/agent/config/selectorEnMaquina.js.map +1 -0
  39. package/dist/agent/{settingsEnDisco.js → config/settingsEnDisco.js} +37 -2
  40. package/dist/agent/config/settingsEnDisco.js.map +1 -0
  41. package/dist/agent/{versionEnDisco.js → config/versionEnDisco.js} +8 -4
  42. package/dist/agent/config/versionEnDisco.js.map +1 -0
  43. package/dist/agent/dispositivos/arranqueDeEmulador.js +146 -0
  44. package/dist/agent/dispositivos/arranqueDeEmulador.js.map +1 -0
  45. package/dist/agent/{dispositivosEnMaquina.js → dispositivos/dispositivosEnMaquina.js} +93 -6
  46. package/dist/agent/dispositivos/dispositivosEnMaquina.js.map +1 -0
  47. package/dist/agent/dispositivos/hotswap.js.map +1 -0
  48. package/dist/agent/dispositivos/instalacionEnMaquina.js.map +1 -0
  49. package/dist/agent/dispositivos/juezVisual.js +152 -0
  50. package/dist/agent/dispositivos/juezVisual.js.map +1 -0
  51. package/dist/agent/{lanzamientoEnMaquina.js → dispositivos/lanzamientoEnMaquina.js} +1 -1
  52. package/dist/agent/dispositivos/lanzamientoEnMaquina.js.map +1 -0
  53. package/dist/agent/{paqueteDelProyecto.js → dispositivos/paqueteDelProyecto.js} +5 -5
  54. package/dist/agent/dispositivos/paqueteDelProyecto.js.map +1 -0
  55. package/dist/agent/dispositivos/procesosEnMaquina.js.map +1 -0
  56. package/dist/agent/dispositivos/sensorDeArranque.js +154 -0
  57. package/dist/agent/dispositivos/sensorDeArranque.js.map +1 -0
  58. package/dist/agent/exportarPdf.js +140 -0
  59. package/dist/agent/exportarPdf.js.map +1 -0
  60. package/dist/agent/{arbolDeProyecto.js → grafo/arbolDeProyecto.js} +2 -2
  61. package/dist/agent/grafo/arbolDeProyecto.js.map +1 -0
  62. package/dist/agent/{artefactosEnDisco.js → grafo/artefactosEnDisco.js} +2 -2
  63. package/dist/agent/grafo/artefactosEnDisco.js.map +1 -0
  64. package/dist/agent/{busquedaRegex.js → grafo/busquedaRegex.js} +41 -1
  65. package/dist/agent/grafo/busquedaRegex.js.map +1 -0
  66. package/dist/agent/grafo/criticaVisual.js +115 -0
  67. package/dist/agent/grafo/criticaVisual.js.map +1 -0
  68. package/dist/agent/grafo/excluirTools.js +82 -0
  69. package/dist/agent/grafo/excluirTools.js.map +1 -0
  70. package/dist/agent/{memoriaDeProyecto.js → grafo/memoriaDeProyecto.js} +1 -1
  71. package/dist/agent/grafo/memoriaDeProyecto.js.map +1 -0
  72. package/dist/agent/grafo/navegacionXone.js +231 -0
  73. package/dist/agent/grafo/navegacionXone.js.map +1 -0
  74. package/dist/agent/{perfiles.js → grafo/perfiles.js} +96 -10
  75. package/dist/agent/grafo/perfiles.js.map +1 -0
  76. package/dist/agent/{proyecto.js → grafo/proyecto.js} +274 -24
  77. package/dist/agent/grafo/proyecto.js.map +1 -0
  78. package/dist/agent/grafo/skills.js +425 -0
  79. package/dist/agent/grafo/skills.js.map +1 -0
  80. package/dist/agent/grafo/xoneAgent.js +566 -0
  81. package/dist/agent/grafo/xoneAgent.js.map +1 -0
  82. package/dist/agent/navegacion/estilosEnDisco.js +110 -0
  83. package/dist/agent/navegacion/estilosEnDisco.js.map +1 -0
  84. package/dist/agent/navegacion/hechosEnDisco.js +26 -0
  85. package/dist/agent/navegacion/hechosEnDisco.js.map +1 -0
  86. package/dist/agent/navegacion/indiceEnDisco.js +36 -0
  87. package/dist/agent/navegacion/indiceEnDisco.js.map +1 -0
  88. package/dist/agent/navegacion/modeloDeProyecto.js +228 -0
  89. package/dist/agent/navegacion/modeloDeProyecto.js.map +1 -0
  90. package/dist/agent/raizDelPaquete.js +56 -0
  91. package/dist/agent/raizDelPaquete.js.map +1 -0
  92. package/dist/agent/{checkpointer.js → sesiones/checkpointer.js} +1 -1
  93. package/dist/agent/sesiones/checkpointer.js.map +1 -0
  94. package/dist/agent/sesiones/git.js.map +1 -0
  95. package/dist/agent/{gitSync.js → sesiones/gitSync.js} +3 -3
  96. package/dist/agent/sesiones/gitSync.js.map +1 -0
  97. package/dist/agent/{sesionGit.js → sesiones/sesionGit.js} +2 -2
  98. package/dist/agent/sesiones/sesionGit.js.map +1 -0
  99. package/dist/agent/subagentes/agentesEnDisco.js +818 -0
  100. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -0
  101. package/dist/agent/subagentes/consumoExterno.js.map +1 -0
  102. package/dist/agent/subagentes/escrituraDeCodex.js.map +1 -0
  103. package/dist/agent/{escrituraDeOpencode.js → subagentes/escrituraDeOpencode.js} +1 -1
  104. package/dist/agent/subagentes/escrituraDeOpencode.js.map +1 -0
  105. package/dist/agent/{escrituraExterna.js → subagentes/escrituraExterna.js} +59 -6
  106. package/dist/agent/subagentes/escrituraExterna.js.map +1 -0
  107. package/dist/agent/subagentes/pluginDeSkills.js +99 -0
  108. package/dist/agent/subagentes/pluginDeSkills.js.map +1 -0
  109. package/dist/agent/subagentes/subagenteCodex.js.map +1 -0
  110. package/dist/agent/{subagenteExterno.js → subagentes/subagenteExterno.js} +48 -0
  111. package/dist/agent/subagentes/subagenteExterno.js.map +1 -0
  112. package/dist/agent/{subagenteOpencode.js → subagentes/subagenteOpencode.js} +53 -1
  113. package/dist/agent/subagentes/subagenteOpencode.js.map +1 -0
  114. package/dist/agent/{aumentador.js → tareas/aumentador.js} +2 -2
  115. package/dist/agent/tareas/aumentador.js.map +1 -0
  116. package/dist/agent/{juezDeTarea.js → tareas/juezDeTarea.js} +6 -6
  117. package/dist/agent/tareas/juezDeTarea.js.map +1 -0
  118. package/dist/agent/{tareasEnDisco.js → tareas/tareasEnDisco.js} +53 -4
  119. package/dist/agent/tareas/tareasEnDisco.js.map +1 -0
  120. package/dist/agent/{diagnosticoDeTools.js → turno/diagnosticoDeTools.js} +5 -1
  121. package/dist/agent/turno/diagnosticoDeTools.js.map +1 -0
  122. package/dist/agent/{guionizado.js → turno/guionizado.js} +1 -1
  123. package/dist/agent/turno/guionizado.js.map +1 -0
  124. package/dist/agent/turno/informeDeTraza.js +245 -0
  125. package/dist/agent/turno/informeDeTraza.js.map +1 -0
  126. package/dist/agent/turno/inspectorDePrompt.js +205 -0
  127. package/dist/agent/turno/inspectorDePrompt.js.map +1 -0
  128. package/dist/agent/{instantanea.js → turno/instantanea.js} +7 -2
  129. package/dist/agent/turno/instantanea.js.map +1 -0
  130. package/dist/agent/{interrupts.js → turno/interrupts.js} +2 -2
  131. package/dist/agent/turno/interrupts.js.map +1 -0
  132. package/dist/agent/turno/mensajes.js.map +1 -0
  133. package/dist/agent/turno/normalizar.js.map +1 -0
  134. package/dist/agent/{puente.js → turno/puente.js} +71 -4
  135. package/dist/agent/turno/puente.js.map +1 -0
  136. package/dist/agent/turno/resumenDeContexto.js +260 -0
  137. package/dist/agent/turno/resumenDeContexto.js.map +1 -0
  138. package/dist/agent/{resumenDeTool.js → turno/resumenDeTool.js} +33 -0
  139. package/dist/agent/turno/resumenDeTool.js.map +1 -0
  140. package/dist/agent/turno/rubrica.js +91 -0
  141. package/dist/agent/turno/rubrica.js.map +1 -0
  142. package/dist/agent/turno/textoDeTool.js.map +1 -0
  143. package/dist/agent/{turnoReal.js → turno/turnoReal.js} +210 -25
  144. package/dist/agent/turno/turnoReal.js.map +1 -0
  145. package/dist/agent/turno/verificador.js.map +1 -0
  146. package/dist/cli/config.js +1 -1
  147. package/dist/cli/config.js.map +1 -1
  148. package/dist/cli/consola.js +72 -9
  149. package/dist/cli/consola.js.map +1 -1
  150. package/dist/cli/describe.js +2 -2
  151. package/dist/cli/describe.js.map +1 -1
  152. package/dist/cli/doctor.js +1 -1
  153. package/dist/cli/doctor.js.map +1 -1
  154. package/dist/cli/fuentesDeDisco.js +29 -0
  155. package/dist/cli/fuentesDeDisco.js.map +1 -0
  156. package/dist/cli/main.js +87 -31
  157. package/dist/cli/main.js.map +1 -1
  158. package/dist/cli/run.js +40 -10
  159. package/dist/cli/run.js.map +1 -1
  160. package/dist/cli/traza.js +48 -0
  161. package/dist/cli/traza.js.map +1 -0
  162. package/dist/cli/tui/correrTui.js +4 -4
  163. package/dist/cli/tui/correrTui.js.map +1 -1
  164. package/dist/cli/verify.js +1 -1
  165. package/dist/cli/verify.js.map +1 -1
  166. package/dist/core/actos.js +1 -1
  167. package/dist/core/adjuntos.js +2 -2
  168. package/dist/core/agentes.js +247 -7
  169. package/dist/core/agentes.js.map +1 -1
  170. package/dist/core/artefactos.js +4 -4
  171. package/dist/core/config.js +1 -1
  172. package/dist/core/contextos.js +12 -1
  173. package/dist/core/contextos.js.map +1 -1
  174. package/dist/core/descargas.js +1 -1
  175. package/dist/core/dispositivos.js +53 -3
  176. package/dist/core/dispositivos.js.map +1 -1
  177. package/dist/core/esfuerzo.js +141 -0
  178. package/dist/core/esfuerzo.js.map +1 -0
  179. package/dist/core/esqueleto.js +1 -1
  180. package/dist/core/estilos.js +78 -0
  181. package/dist/core/estilos.js.map +1 -0
  182. package/dist/core/exportacion.js +140 -0
  183. package/dist/core/exportacion.js.map +1 -0
  184. package/dist/core/hechosDelProyecto.js +87 -0
  185. package/dist/core/hechosDelProyecto.js.map +1 -0
  186. package/dist/core/logDeDispositivo.js +143 -0
  187. package/dist/core/logDeDispositivo.js.map +1 -0
  188. package/dist/core/modelos.js +48 -3
  189. package/dist/core/modelos.js.map +1 -1
  190. package/dist/core/mudanzaDeWorkspace.js +93 -0
  191. package/dist/core/mudanzaDeWorkspace.js.map +1 -0
  192. package/dist/core/navegacion.js +134 -0
  193. package/dist/core/navegacion.js.map +1 -0
  194. package/dist/core/notify.js +5 -0
  195. package/dist/core/notify.js.map +1 -1
  196. package/dist/core/planDeSubida.js +1 -1
  197. package/dist/core/planes.js +70 -0
  198. package/dist/core/planes.js.map +1 -0
  199. package/dist/core/ports.js +4 -0
  200. package/dist/core/ports.js.map +1 -1
  201. package/dist/core/rubrica.js +90 -0
  202. package/dist/core/rubrica.js.map +1 -0
  203. package/dist/core/selectorDeCarpeta.js +87 -0
  204. package/dist/core/selectorDeCarpeta.js.map +1 -0
  205. package/dist/core/settings.js +56 -7
  206. package/dist/core/settings.js.map +1 -1
  207. package/dist/core/shellDeAgente.js +144 -0
  208. package/dist/core/shellDeAgente.js.map +1 -0
  209. package/dist/core/skills.js +158 -0
  210. package/dist/core/skills.js.map +1 -0
  211. package/dist/core/topeAgotado.js +69 -0
  212. package/dist/core/topeAgotado.js.map +1 -0
  213. package/dist/core/zipDeSkill.js +107 -0
  214. package/dist/core/zipDeSkill.js.map +1 -0
  215. package/dist/web/servidor/arranque.js +853 -54
  216. package/dist/web/servidor/arranque.js.map +1 -1
  217. package/dist/web/servidor/corredorDeTareas.js +3 -3
  218. package/dist/web/servidor/corredorDeTareas.js.map +1 -1
  219. package/dist/web/servidor/sesiones.js +34 -6
  220. package/dist/web/servidor/sesiones.js.map +1 -1
  221. package/dist/web/servidor/transporte.js +1 -1
  222. package/dist/web/servidor/transporte.js.map +1 -1
  223. package/dist/web/servidor/vestibulo.js +62 -13
  224. package/dist/web/servidor/vestibulo.js.map +1 -1
  225. package/package.json +7 -4
  226. package/skills/archify/SKILL.md +25 -2
  227. package/skills/artifacts-builder/SKILL.md +23 -0
  228. package/skills/xone-hotswap/SKILL.md +96 -0
  229. package/skills/xone-hotswap/scripts/xone-arrancar-android +202 -0
  230. package/skills/xone-hotswap/scripts/xone-arrancar-ios +85 -0
  231. package/skills/xone-hotswap/scripts/xone-captura-android +93 -0
  232. package/skills/xone-hotswap/scripts/xone-desplegar-android +439 -0
  233. package/skills/xone-hotswap/scripts/xone-hotswap +201 -0
  234. package/skills/xone-hotswap/scripts/xone-log-android +102 -0
  235. package/skills/xone-hotswap/scripts/xone-reiniciar-android +132 -0
  236. package/skills/xone-plan-builder/SKILL.md +22 -4
  237. package/skills/xone-spec-builder/SKILL.md +37 -7
  238. package/apps/web/dist/assets/index-CFYAoHdw.js +0 -477
  239. package/dist/agent/agentesEnDisco.js +0 -539
  240. package/dist/agent/agentesEnDisco.js.map +0 -1
  241. package/dist/agent/arbolDeProyecto.js.map +0 -1
  242. package/dist/agent/artefactosEnDisco.js.map +0 -1
  243. package/dist/agent/aumentador.js.map +0 -1
  244. package/dist/agent/authEnDisco.js.map +0 -1
  245. package/dist/agent/busquedaRegex.js.map +0 -1
  246. package/dist/agent/catalogoModelos.js.map +0 -1
  247. package/dist/agent/checkpointer.js.map +0 -1
  248. package/dist/agent/cloudstudioClient.js.map +0 -1
  249. package/dist/agent/cloudstudioMcp.js.map +0 -1
  250. package/dist/agent/configEnDisco.js.map +0 -1
  251. package/dist/agent/consumoExterno.js.map +0 -1
  252. package/dist/agent/crearProyecto.js.map +0 -1
  253. package/dist/agent/descarga.js.map +0 -1
  254. package/dist/agent/diagnosticoDeTools.js.map +0 -1
  255. package/dist/agent/dispositivosEnMaquina.js.map +0 -1
  256. package/dist/agent/entorno.js.map +0 -1
  257. package/dist/agent/escrituraDeCodex.js.map +0 -1
  258. package/dist/agent/escrituraDeOpencode.js.map +0 -1
  259. package/dist/agent/escrituraExterna.js.map +0 -1
  260. package/dist/agent/gemini.js.map +0 -1
  261. package/dist/agent/git.js.map +0 -1
  262. package/dist/agent/gitSync.js.map +0 -1
  263. package/dist/agent/guionizado.js.map +0 -1
  264. package/dist/agent/hotswap.js.map +0 -1
  265. package/dist/agent/instalacionEnMaquina.js.map +0 -1
  266. package/dist/agent/instantanea.js.map +0 -1
  267. package/dist/agent/interrupts.js.map +0 -1
  268. package/dist/agent/juezDeTarea.js.map +0 -1
  269. package/dist/agent/lanzamientoEnMaquina.js.map +0 -1
  270. package/dist/agent/manifiesto.js.map +0 -1
  271. package/dist/agent/memoriaDeProyecto.js.map +0 -1
  272. package/dist/agent/mensajes.js.map +0 -1
  273. package/dist/agent/modelos.js +0 -130
  274. package/dist/agent/modelos.js.map +0 -1
  275. package/dist/agent/modelosDeMotor.js.map +0 -1
  276. package/dist/agent/normalizar.js.map +0 -1
  277. package/dist/agent/paqueteDelProyecto.js.map +0 -1
  278. package/dist/agent/perfiles.js.map +0 -1
  279. package/dist/agent/persona.js.map +0 -1
  280. package/dist/agent/procesosEnMaquina.js.map +0 -1
  281. package/dist/agent/proyecto.js.map +0 -1
  282. package/dist/agent/puente.js.map +0 -1
  283. package/dist/agent/resumenDeContexto.js +0 -30
  284. package/dist/agent/resumenDeContexto.js.map +0 -1
  285. package/dist/agent/resumenDeTool.js.map +0 -1
  286. package/dist/agent/sesionGit.js.map +0 -1
  287. package/dist/agent/settingsEnDisco.js.map +0 -1
  288. package/dist/agent/skills.js +0 -65
  289. package/dist/agent/skills.js.map +0 -1
  290. package/dist/agent/subagenteCodex.js.map +0 -1
  291. package/dist/agent/subagenteExterno.js.map +0 -1
  292. package/dist/agent/subagenteOpencode.js.map +0 -1
  293. package/dist/agent/subida.js.map +0 -1
  294. package/dist/agent/tareasEnDisco.js.map +0 -1
  295. package/dist/agent/textoDeTool.js.map +0 -1
  296. package/dist/agent/turnoReal.js.map +0 -1
  297. package/dist/agent/verificador.js.map +0 -1
  298. package/dist/agent/versionEnDisco.js.map +0 -1
  299. package/dist/agent/xoneAgent.js +0 -347
  300. package/dist/agent/xoneAgent.js.map +0 -1
  301. package/dist/agent/zip.js.map +0 -1
  302. /package/dist/agent/{manifiesto.js → cloudstudio/manifiesto.js} +0 -0
  303. /package/dist/agent/{entorno.js → config/entorno.js} +0 -0
  304. /package/dist/agent/{gemini.js → config/gemini.js} +0 -0
  305. /package/dist/agent/{hotswap.js → dispositivos/hotswap.js} +0 -0
  306. /package/dist/agent/{instalacionEnMaquina.js → dispositivos/instalacionEnMaquina.js} +0 -0
  307. /package/dist/agent/{procesosEnMaquina.js → dispositivos/procesosEnMaquina.js} +0 -0
  308. /package/dist/agent/{git.js → sesiones/git.js} +0 -0
  309. /package/dist/agent/{consumoExterno.js → subagentes/consumoExterno.js} +0 -0
  310. /package/dist/agent/{escrituraDeCodex.js → subagentes/escrituraDeCodex.js} +0 -0
  311. /package/dist/agent/{subagenteCodex.js → subagentes/subagenteCodex.js} +0 -0
  312. /package/dist/agent/{mensajes.js → turno/mensajes.js} +0 -0
  313. /package/dist/agent/{normalizar.js → turno/normalizar.js} +0 -0
  314. /package/dist/agent/{textoDeTool.js → turno/textoDeTool.js} +0 -0
  315. /package/dist/agent/{verificador.js → turno/verificador.js} +0 -0
@@ -33,9 +33,12 @@ import { basename, dirname, join } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { randomUUID } from "node:crypto";
35
35
  import { homedir } from "node:os";
36
- import { escribirAgente, leerAgente } from "../../core/agentes.js";
37
- import { borrarAgente, cargarAgentes, guardarAgente } from "../../agent/agentesEnDisco.js";
38
- import { detectarDispositivos, frameworkEnDispositivo } from "../../agent/dispositivosEnMaquina.js";
36
+ import { escribirAgente, leerAgente, motivoDeNombreInaceptable } from "../../core/agentes.js";
37
+ import { motivoDeNombreDeSkillInaceptable, nombreDeSkillSugerido, } from "../../core/skills.js";
38
+ import { borrarSkill, cargarSkills, esDeSerie as esDeSerieLaSkill, guardarSkill, instalarSkillDesdeZip, } from "../../agent/grafo/skills.js";
39
+ import { TOPE_DEL_ZIP } from "../../core/zipDeSkill.js";
40
+ import { borrarAgente, cargarAgentes, esDeSerie, guardarAgente, renombrarAgente, restaurarAgente, } from "../../agent/subagentes/agentesEnDisco.js";
41
+ import { detectarDispositivos, frameworkEnDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
39
42
  /**
40
43
  * El veredicto de «¿se puede lanzar?» y el lanzamiento entero.
41
44
  *
@@ -45,48 +48,53 @@ import { detectarDispositivos, frameworkEnDispositivo } from "../../agent/dispos
45
48
  * donde puede dejar de estar montado, que es el patrón de fallo de esta casa. Lo único que este
46
49
  * lado aporta es `alFase`, que es cómo se cuenta el recorrido.
47
50
  */
48
- import { lanzarEnDispositivo, } from "../../agent/lanzamientoEnMaquina.js";
51
+ import { lanzarEnDispositivo, } from "../../agent/dispositivos/lanzamientoEnMaquina.js";
49
52
  import { motivoDeBloqueo, puedeLanzarse, } from "../../core/puedeLanzarse.js";
50
53
  import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
51
- import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivosEnMaquina.js";
52
- import { correrPasoDeReceta } from "../../agent/instalacionEnMaquina.js";
53
- import { modelosDeMotor } from "../../agent/modelosDeMotor.js";
54
+ import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
55
+ import { arrancarEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
56
+ import { correrPasoDeReceta } from "../../agent/dispositivos/instalacionEnMaquina.js";
57
+ import { modelosDeMotor } from "../../agent/config/modelosDeMotor.js";
54
58
  import { parsear, compatibleConOpenAi, esProveedorPersonalizado, idDeProveedorPersonalizado, motivoDeEndpointInaceptable, motivoDeSlugInaceptable, nombreDeProveedor, PROVEEDORES, slugDesdeNombre, resolver, SIN_CREDENCIAL, PAPELES, } from "../../core/modelos.js";
55
59
  import { hayCredencial } from "../../cli/consola.js";
56
60
  import { motivoDeClaveInaceptable } from "../../core/config.js";
57
- import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarModeloGlobal, } from "../../agent/configEnDisco.js";
58
- import { borrarCredencial, guardarCredencial } from "../../agent/authEnDisco.js";
59
- import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/configEnDisco.js";
60
- import { crearCheckpointerDeProyecto, hayCheckpoint, olvidarHilo, } from "../../agent/checkpointer.js";
61
- import { cargarSettings, guardarConcurrenciaDeTareas, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, } from "../../agent/settingsEnDisco.js";
62
- import { dentroDelWorkspace, seAplicaSinAprobacion } from "../../core/settings.js";
63
- import { cloudstudioDelProyecto } from "../../agent/configEnDisco.js";
64
- import { abrirEnSistema } from "../../agent/cloudstudioMcp.js";
65
- import { nombreDePersona } from "../../agent/persona.js";
66
- import { cambiosDeSesion, fotoDeApertura, olvidarSesion, parcheDeSesion } from "../../agent/sesionGit.js";
67
- import { commitDeTurno, cambiosPendientes, trabajoSinCommitear } from "../../agent/gitSync.js";
61
+ import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarModeloGlobal, } from "../../agent/config/configEnDisco.js";
62
+ import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDisco.js";
63
+ import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
64
+ import { crearCheckpointerDeProyecto, hayCheckpoint, olvidarHilo, } from "../../agent/sesiones/checkpointer.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";
69
+ import { cloudstudioDelProyecto } from "../../agent/config/configEnDisco.js";
70
+ import { abrirEnSistema } from "../../agent/cloudstudio/cloudstudioMcp.js";
71
+ import { nombreDePersona } from "../../agent/config/persona.js";
72
+ import { cambiosDeSesion, fotoDeApertura, olvidarSesion, parcheDeSesion } from "../../agent/sesiones/sesionGit.js";
73
+ import { commitDeTurno, cambiosPendientes, trabajoSinCommitear } from "../../agent/sesiones/gitSync.js";
68
74
  import { marcarTareaDeSesion, sembrarConsumosPendientes } from "./sesiones.js";
69
75
  import { RUTA_ARTEFACTOS, esRutaDeArtefacto } from "../../core/artefactos.js";
70
- import { arbolDeProyecto, leerFicheroDeProyecto, motivoDeRutaInaceptable, } from "../../agent/arbolDeProyecto.js";
71
- import { leerArtefactoCrudo, leerArtefactoDeSesion, } from "../../agent/artefactosEnDisco.js";
72
- import { CatalogoModelos } from "../../agent/catalogoModelos.js";
73
- import { Modelos } from "../../agent/modelos.js";
74
- import { crearJuezDeTarea, invocarConModelos } from "../../agent/juezDeTarea.js";
76
+ import { arbolDeProyecto, leerFicheroDeProyecto, motivoDeRutaInaceptable, } from "../../agent/grafo/arbolDeProyecto.js";
77
+ import { leerArtefactoCrudo, leerArtefactoDeSesion, } from "../../agent/grafo/artefactosEnDisco.js";
78
+ import { CatalogoModelos, baseUrlDeOllama, baseUrlDeOllamaCloud, capacidadesDeOllama, } from "../../agent/config/catalogoModelos.js";
79
+ import { Modelos } from "../../agent/config/modelos.js";
80
+ import { crearJuezDeTarea, invocarConModelos } from "../../agent/tareas/juezDeTarea.js";
81
+ import { SIN_CONSUMO } from "../../agent/subagentes/consumoExterno.js";
75
82
  import { AumentadorGuionizado } from "../../core/ports.js";
76
83
  import { arrancarServidor } from "./servidor.js";
77
84
  import { consolaParaTarea, crearCorredorDeTareas, revisionConGit, } from "./corredorDeTareas.js";
78
85
  import { CONCURRENCIA_POR_OMISION, conEstado, darPorBuenaAMano, tituloDeTarea, } from "../../core/tareas.js";
79
- import { aplicarFeedback, TOPE_DE_ADJUNTO } from "../../agent/tareasEnDisco.js";
86
+ import { aplicarFeedback, TOPE_DE_ADJUNTO } from "../../agent/tareas/tareasEnDisco.js";
80
87
  import { nombreDeAdjuntoAceptable } from "../../core/adjuntos.js";
81
- import { rutaMemoriaDeProyecto } from "../../agent/memoriaDeProyecto.js";
82
- import { crearAumentador, invocarParaAumentar } from "../../agent/aumentador.js";
83
- import { baseDeWorkspacePorOmision, conexionDeVestibulo, crearVestibulo, escribirProyectoEnDisco, esProyectoEnDisco, } from "./vestibulo.js";
88
+ import { rutaMemoriaDeProyecto } from "../../agent/grafo/memoriaDeProyecto.js";
89
+ import { crearAumentador, invocarParaAumentar } from "../../agent/tareas/aumentador.js";
90
+ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo, crearVestibulo, escribirProyectoEnDisco, esProyectoEnDisco, } from "./vestibulo.js";
84
91
  // Valor y no tipo: la traducción de una `Tarea` a lo que viaja vive JUNTO al tipo que
85
92
  // produce y no aquí. Estuvo en este cierre, y ahí se cayó `veredicto` sin que nada se
86
93
  // pusiera rojo (F1 de la revisión final) — el patrón de fallo que este repo ya tiene
87
94
  // documentado cinco veces: una composición de producción dentro de algo que los tests
88
95
  // doblan.
89
96
  import { filaDeTarea } from "./transporte.js";
97
+ import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
90
98
  /** Las dos rutas del cable. El cliente las tiene escritas en `apps/web/src/conexion.ts`. */
91
99
  /**
92
100
  * Cuánto se espera al aviso de «lo que ya había sin commitear» antes de anunciar sin él.
@@ -97,6 +105,24 @@ import { filaDeTarea } from "./transporte.js";
97
105
  * definición de que el aviso frena, que es justo lo que su propia regla prohíbe.
98
106
  */
99
107
  export const MS_DE_TRABAJO_AL_ABRIR = 2000;
108
+ /**
109
+ * Cuánto se espera al arranque antes de entrar igual.
110
+ *
111
+ * Lo que se prepara es la sesión MCP y la lista de proyectos del primer entorno registrado,
112
+ * y el usuario pidió no entrar al Escritorio hasta tenerlo: entrar antes significa un
113
+ * Escritorio vacío que se rellena delante. Pero esperar SIN plazo es lo que convierte una
114
+ * espera en un cuelgue — medido con un MCP que descarta paquetes, el lienzo se quedaba para
115
+ * siempre, y eso es peor que lo que veníamos a arreglar.
116
+ *
117
+ * Generoso a propósito: una sesión MCP fría lleva descubrimiento OAuth y handshake, y
118
+ * cortarla pronto haría entrar en vacío justo a quien tiene la red lenta, que es a quien más
119
+ * le duele. Y como el lienzo DICE lo que está haciendo (`preparando`), la espera se lee como
120
+ * trabajo y no como una pantalla muerta.
121
+ *
122
+ * Igual que `MS_DE_TRABAJO_AL_ABRIR`, lo que se acota es la ESPERA y no el trabajo: la
123
+ * promesa sigue viva y lo que llegue tarde sale en el siguiente anuncio.
124
+ */
125
+ export const MS_DE_PREPARACION = 10_000;
100
126
  export const RUTA_EVENTOS = "/eventos";
101
127
  export const RUTA_ACCION = "/accion";
102
128
  /**
@@ -109,6 +135,13 @@ export const RUTA_ACCION = "/accion";
109
135
  * el contrato de las skills que los escriben es «autocontenido».
110
136
  */
111
137
  export const RUTA_ARTEFACTO = "/artefacto";
138
+ /**
139
+ * `POST /skill?nombre=<fichero.zip>&ambito=<global|proyecto>` — los BYTES de un `.zip`.
140
+ *
141
+ * Por HTTP y no por el cable, que lleva JSON: la misma razón y el mismo molde que
142
+ * `POST /adjunto`. Una ruta EXACTA, porque el servidor casa por ruta exacta.
143
+ */
144
+ export const RUTA_SKILL = "/skill";
112
145
  /**
113
146
  * `POST /adjunto?tarea=<id>&nombre=<fichero>` — los BYTES de un adjunto.
114
147
  *
@@ -245,6 +278,14 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
245
278
  * en la Trayectoria —la otra pestaña—, y el wizard repintaba el mismo paso sin decir nada.
246
279
  */
247
280
  let aviso;
281
+ /**
282
+ * Qué está preparando el arranque, mientras lo prepara. `undefined` = listo.
283
+ *
284
+ * Vive aquí y no en el anuncio porque el `alta` se emite también en los dos flancos de
285
+ * cada turno, y esos anuncios no están preparando nada: quien lo pone y lo quita es el
286
+ * arranque de la conexión, y los demás leen lo que haya.
287
+ */
288
+ let preparando;
248
289
  const destinoActual = () => vestibulo.proyectoAbierto() ?? vestibulo.consola;
249
290
  const emitir = (mensaje) => {
250
291
  for (const cliente of clientes)
@@ -436,6 +477,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
436
477
  : {}),
437
478
  ...(vestibulo.nombre === undefined ? {} : { nombre: vestibulo.nombre }),
438
479
  ...(aviso === undefined ? {} : { aviso }),
480
+ // Ausente = listo, y solo entonces el cliente sale del lienzo. Se lee de la variable
481
+ // en cada anuncio en vez de pasarse por parámetro: los anuncios de los flancos del
482
+ // turno no preparan nada y tienen que decir lo que hay, no lo que había.
483
+ ...(preparando === undefined ? {} : { preparando }),
439
484
  });
440
485
  };
441
486
  /**
@@ -444,6 +489,18 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
444
489
  * recuerda —como fallo— hasta que alguien lo vuelva a pedir a propósito.
445
490
  */
446
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();
447
504
  /**
448
505
  * El estado de modelos: qué está en vigor y qué se puede elegir.
449
506
  *
@@ -451,7 +508,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
451
508
  * proyecto abierto no hay sesión y por tanto no hay modelo que afirmar: el campo se va
452
509
  * y el cliente pinta «Elige modelo» en vez de una fila muerta.
453
510
  */
454
- 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
+ };
455
518
  /**
456
519
  * Los subagentes en vigor, compuestos pero sin mandar.
457
520
  *
@@ -471,9 +534,17 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
471
534
  motor: a.motor,
472
535
  ...(a.modelo === undefined ? {} : { modelo: a.modelo }),
473
536
  soloLectura: a.soloLectura,
537
+ // Ausente = no la tiene. No se emite `false` para que el cliente no tenga que
538
+ // distinguir entre «no» y «no consta»: aquí son lo mismo y la ausencia lo dice.
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 }),
474
542
  skills: a.skills,
475
543
  instrucciones: a.instrucciones,
476
544
  origen: a.origen,
545
+ // Ausente cuando el `.md` es del usuario, y eso es el dato: es lo que le pone la
546
+ // papelera en vez de un «Restaurar el de serie».
547
+ ...(a.semilla === undefined ? {} : { semilla: a.semilla }),
477
548
  })),
478
549
  problemas,
479
550
  };
@@ -489,6 +560,52 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
489
560
  */
490
561
  const atenderAgente = (mensaje) => {
491
562
  const abierto = vestibulo.proyectoAbierto();
563
+ /**
564
+ * `restaurar` va ANTES de la guarda del ámbito porque no tiene ámbito que elegir: la
565
+ * siembra solo escribe en el global, así que la versión de serie solo puede volver ahí.
566
+ * Pasarlo por el `ambito` del mensaje sería ofrecer una decisión que no existe, y con un
567
+ * `ambito: "proyecto"` daría un «no hay ningún proyecto abierto» que no viene a cuento.
568
+ */
569
+ if (mensaje.accion === "restaurar") {
570
+ informar(restaurarAgente(homedir(), mensaje.agente.nombre)
571
+ ? `subagente «${mensaje.agente.nombre}» restaurado al de serie`
572
+ : `«${mensaje.agente.nombre}» no es uno de los de serie: no hay ninguna versión nuestra que poner`);
573
+ emitirAgentes();
574
+ return;
575
+ }
576
+ /**
577
+ * Y un de serie NO se borra. La negativa vive AQUÍ y no solo en el cliente, que le
578
+ * esconde el icono: esconderlo es presentación, y este mensaje lo puede mandar cualquier
579
+ * cosa que hable por el cable. Borrarlo no devolvía el de serie —`sembrarAgentes` no
580
+ * resiembra lo que consta entregado— así que era perderlo para siempre; lo que sí hace
581
+ * lo que aquello prometía es `restaurar`, y se dice en el mismo sitio donde se rechaza.
582
+ */
583
+ if (mensaje.accion === "borrar" && mensaje.ambito === "global" && esDeSerie(mensaje.agente.nombre)) {
584
+ informar(`«${mensaje.agente.nombre}» lo trae xonecode y no se borra: edítalo, o restaura el de serie si quieres el original`);
585
+ return;
586
+ }
587
+ /**
588
+ * Un renombrado de verdad: `renombrandoDe` puesto y DISTINTO del nombre nuevo. Con el
589
+ * mismo nombre es un guardado normal, y tratarlo de renombrado lo haría fallar por
590
+ * «destino ocupado» contra sí mismo.
591
+ */
592
+ const renombrandoDe = mensaje.accion === "guardar" &&
593
+ mensaje.renombrandoDe !== undefined &&
594
+ mensaje.renombrandoDe !== mensaje.agente.nombre
595
+ ? mensaje.renombrandoDe
596
+ : undefined;
597
+ /**
598
+ * Y un de serie tampoco se RENOMBRA, por lo mismo que no se borra: su nombre es lo que lo
599
+ * ata a la marca de la siembra (`.semilla.json` guarda el hash POR NOMBRE), así que
600
+ * moverlo lo convierte en un subagente del usuario y hace que el de serie vuelva a
601
+ * sembrarse al arrancar — dos especialistas donde había uno. La regla vive aquí y no en
602
+ * `renombrarAgente`, que mueve un fichero: dos sitios donde decidir lo mismo es cómo uno
603
+ * de los dos se queda sin la regla.
604
+ */
605
+ if (renombrandoDe !== undefined && mensaje.ambito === "global" && esDeSerie(renombrandoDe)) {
606
+ informar(`«${renombrandoDe}» lo trae xonecode y su nombre no se cambia: es lo que lo ata a la versión de serie`);
607
+ return;
608
+ }
492
609
  if (mensaje.ambito === "proyecto" && abierto === undefined) {
493
610
  informar("no hay ningún proyecto abierto: ese subagente solo se puede guardar como global");
494
611
  return;
@@ -515,8 +632,35 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
515
632
  informar(`no se guarda «${candidato.nombre}»: ${comprobado.error}`);
516
633
  return;
517
634
  }
518
- guardarAgente(base, candidato);
519
- informar(`subagente «${candidato.nombre}» guardado en ${mensaje.ambito}`);
635
+ /**
636
+ * Y el NOMBRE tiene que ser un slug, aquí y no en el cargador: cargando solo se DICE
637
+ * (`leerCarpetaDeAgentes`), porque rechazar al cargar haría desaparecer un subagente
638
+ * que funciona. Guardar es el único momento con alguien delante para arreglarlo.
639
+ *
640
+ * No lo cubre `leerAgente`: el nombre no sale del `.md`, sale del FICHERO, así que
641
+ * escribir y volver a leer no lo mira. Y tampoco `segmentoSeguro`, que impide salir de
642
+ * la carpeta y nada más — `Documentador` pasa esa y no es un slug.
643
+ */
644
+ const malNombre = motivoDeNombreInaceptable(candidato.nombre);
645
+ if (malNombre !== undefined) {
646
+ informar(`no se guarda «${candidato.nombre}»: el nombre ${malNombre}`);
647
+ return;
648
+ }
649
+ if (renombrandoDe === undefined) {
650
+ guardarAgente(base, candidato);
651
+ informar(`subagente «${candidato.nombre}» guardado en ${mensaje.ambito}`);
652
+ }
653
+ else {
654
+ // Los tres desenlaces se dicen distintos: el destino ocupado y el origen que ya no
655
+ // está son dos situaciones que el usuario arregla de formas distintas, y un «no se
656
+ // pudo» para las dos le haría probar la equivocada.
657
+ const como = renombrarAgente(base, renombrandoDe, candidato);
658
+ informar(como === "hecho"
659
+ ? `subagente «${renombrandoDe}» renombrado a «${candidato.nombre}»`
660
+ : como === "sin-origen"
661
+ ? `«${renombrandoDe}» ya no existía en ${mensaje.ambito}: no se ha renombrado`
662
+ : `ya hay un subagente «${candidato.nombre}» en ${mensaje.ambito}: elige otro nombre, o bórralo primero`);
663
+ }
520
664
  }
521
665
  }
522
666
  catch (error) {
@@ -524,6 +668,132 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
524
668
  }
525
669
  emitirAgentes();
526
670
  };
671
+ /**
672
+ * Las skills en vigor, compuestas pero sin mandar.
673
+ *
674
+ * `cargarSkills` cada vez y no una lista cacheada, por lo mismo que `mensajeDeAgentes`:
675
+ * las carpetas se pueden tocar a mano con la consola abierta, y una lista congelada al
676
+ * arrancar haría creer que el cambio no se aplicó.
677
+ *
678
+ * **El `cuerpo` viaja solo en las del USUARIO.** Las de serie son nueve ficheros que pasan
679
+ * de veinte mil caracteres cada uno; mandarlos en la ráfaga de bienvenida es pagar cientos
680
+ * de kilobytes por rellenar un formulario que nadie puede guardar. El suyo se pide a mano
681
+ * (`cuerpoDeSkill`), que es cuando alguien la está mirando de verdad.
682
+ */
683
+ const mensajeDeSkills = () => {
684
+ const abierto = vestibulo.proyectoAbierto();
685
+ const { skills, problemas } = cargarSkills(abierto?.raiz);
686
+ return {
687
+ clase: "skills",
688
+ skills: skills.map((s) => ({
689
+ nombre: s.nombre,
690
+ descripcion: s.descripcion,
691
+ origen: s.origen,
692
+ tokens: s.tokens,
693
+ ficheros: s.ficheros,
694
+ // El frontmatter va SIEMPRE: son cuatro líneas, y es lo que contesta qué declara el
695
+ // fichero. El CUERPO no, en las de serie: eso sí son decenas de miles de caracteres.
696
+ ...(s.frontmatter === undefined ? {} : { frontmatter: s.frontmatter }),
697
+ ...(s.origen === "serie" ? {} : { cuerpo: s.cuerpo }),
698
+ })),
699
+ problemas,
700
+ };
701
+ };
702
+ const emitirSkills = () => emitir(mensajeDeSkills());
703
+ /**
704
+ * El cuerpo de UNA skill, bajo demanda: al abrir la ficha de una de serie o al copiarla.
705
+ *
706
+ * Se contesta SOLO a quien lo pidió no se puede —el transporte habla con todos—, así que
707
+ * va a todos y el cliente se queda con la que tiene abierta. Es un fichero de texto que ya
708
+ * está en la lista, no un secreto.
709
+ *
710
+ * Lo que no se pudo leer se DICE con un `error`: una ficha que se abre en blanco se lee
711
+ * como que la skill está vacía, y entonces copiarla produciría una copia vacía.
712
+ */
713
+ const atenderCuerpoDeSkill = (mensaje) => {
714
+ const abierto = vestibulo.proyectoAbierto();
715
+ const skill = cargarSkills(abierto?.raiz).skills.find((s) => s.nombre === mensaje.nombre);
716
+ emitir(skill === undefined
717
+ ? { clase: "cuerpoDeSkill", nombre: mensaje.nombre, error: "ya no está en el catálogo" }
718
+ : { clase: "cuerpoDeSkill", nombre: mensaje.nombre, cuerpo: skill.cuerpo });
719
+ };
720
+ /**
721
+ * Alta, cambio y borrado de una skill del usuario. El mismo molde que `atenderAgente`.
722
+ *
723
+ * **Las de serie no se tocan, y la barrera está AQUÍ.** Viven dentro del paquete
724
+ * instalado: una edición se la llevaría el siguiente `npm install` sin decir nada, y un
725
+ * borrado no se puede reponer porque nada nuestro se copia nunca a la casa del usuario
726
+ * —no hay marca de siembra que consultar, a diferencia de los subagentes—. Lo que la
727
+ * consola ofrece sobre una de serie es COPIARLA, que es un `guardar` con otro nombre.
728
+ *
729
+ * `guardarSkill` DEVUELVE el motivo en vez de lanzar, así que los tres desenlaces —el
730
+ * nombre ocupado, el nombre de una de serie y el fallo de escritura— se dicen distintos:
731
+ * son tres cosas que el usuario arregla de tres formas.
732
+ */
733
+ const atenderSkill = (mensaje) => {
734
+ const abierto = vestibulo.proyectoAbierto();
735
+ const nombre = mensaje.skill.nombre;
736
+ if (mensaje.ambito === "proyecto" && abierto === undefined) {
737
+ informar("no hay ningún proyecto abierto: esa skill solo se puede guardar como global");
738
+ return;
739
+ }
740
+ const base = mensaje.ambito === "proyecto" ? abierto.raiz : homedir();
741
+ /**
742
+ * Borrar se INTENTA y el motivo sale de lo que pasó, no de una guarda previa por NOMBRE.
743
+ *
744
+ * La guarda por nombre era demasiado ancha: una skill del usuario que se llame como una
745
+ * de serie —tapándola, que es la forma de afinarla sin editar donde el `npm install` la
746
+ * pisaría— no se podía borrar nunca, porque `esDeSerie` mira el catálogo del paquete y no
747
+ * la carpeta del usuario. Lo que hay en la carpeta del usuario es del usuario y se borra;
748
+ * lo que no está ahí y ADEMÁS lo traemos nosotros es lo que se niega, con el camino al
749
+ * lado. Un solo sitio decide, que es `borrarSkill` contestando si había algo.
750
+ */
751
+ if (mensaje.accion === "borrar") {
752
+ informar(borrarSkill(base, nombre)
753
+ ? `skill «${nombre}» borrada`
754
+ : esDeSerieLaSkill(nombre)
755
+ ? `la skill «${nombre}» la trae xonecode y no se borra: cópiala si quieres partir de ella`
756
+ : `«${nombre}» no existía en ${mensaje.ambito}`);
757
+ emitirSkills();
758
+ return;
759
+ }
760
+ /**
761
+ * El nombre tiene que ser un slug, y se comprueba al GUARDAR y no al cargar: rechazar al
762
+ * cargar haría desaparecer una skill que funciona, y guardar es el único momento con
763
+ * alguien delante para arreglarlo. `segmentoSeguro` —que `guardarSkill` sí aplica— solo
764
+ * impide salir de la carpeta: `Mi Skill` la pasa y no es un slug, y el nombre de una
765
+ * skill viaja además dentro del `.md` de un subagente.
766
+ */
767
+ const malNombre = motivoDeNombreDeSkillInaceptable(nombre);
768
+ if (malNombre !== undefined) {
769
+ const sugerido = nombreDeSkillSugerido(nombre);
770
+ informar(`no se guarda «${nombre}»: el nombre ${malNombre}` +
771
+ (sugerido === undefined ? "" : ` (prueba «${sugerido}»)`));
772
+ return;
773
+ }
774
+ if (mensaje.skill.descripcion.trim() === "") {
775
+ // Es lo que el modelo lee para decidir si le sirve: sin ella la skill está en el
776
+ // catálogo y no la usa nadie nunca, que es peor que no estar.
777
+ informar(`no se guarda «${nombre}»: sin descripción, el orquestador no sabría cuándo usarla`);
778
+ return;
779
+ }
780
+ const renombrandoDe = mensaje.renombrandoDe !== undefined && mensaje.renombrandoDe !== nombre
781
+ ? mensaje.renombrandoDe
782
+ : undefined;
783
+ const escrita = guardarSkill(base, {
784
+ nombre,
785
+ descripcion: mensaje.skill.descripcion,
786
+ cuerpo: mensaje.skill.cuerpo ?? "",
787
+ // El frontmatter de ANTES, para conservar lo que no editamos. Ausente en un alta.
788
+ ...(mensaje.skill.frontmatter === undefined ? {} : { frontmatter: mensaje.skill.frontmatter }),
789
+ }, mensaje.renombrandoDe);
790
+ informar("error" in escrita
791
+ ? `no se guarda «${nombre}»: ${escrita.error}`
792
+ : renombrandoDe === undefined
793
+ ? `skill «${nombre}» guardada en ${mensaje.ambito}`
794
+ : `skill «${renombrandoDe}» renombrada a «${nombre}»`);
795
+ emitirSkills();
796
+ };
527
797
  /** El mensaje de modelos, compuesto pero sin mandar: `adjuntar` se lo da SOLO al cliente
528
798
  * que acaba de llegar, y el resto de sitios lo emite a todos. */
529
799
  const mensajeDeModelos = () => {
@@ -534,10 +804,43 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
534
804
  // pueda leer el config global. Sin el segundo, Ajustes enseñaría «sin elegir» sobre una
535
805
  // máquina con un defecto escrito, justo en la pantalla donde se configura.
536
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;
537
819
  return {
538
820
  clase: "modelos",
539
821
  ...(trabajo === undefined ? {} : { actual: `${trabajo.proveedor}/${trabajo.modelo}` }),
540
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
+ }),
541
844
  proveedores: [...PROVEEDORES, ...idsPersonalizados()].map((p) => ({
542
845
  id: p,
543
846
  nombre: nombreDeProveedor(p, personalizados()),
@@ -598,13 +901,34 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
598
901
  for (const cliente of destinatarios)
599
902
  actos = destino.conectar(cliente);
600
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();
601
918
  // Los subagentes van en la ráfaga por lo mismo que los modelos: la ventana de ajustes
602
919
  // se puede abrir en cuanto conecta, y sin esto enseñaría una lista vacía hasta que algo
603
920
  // los cambiara — que es indistinguible de «no tienes ninguno».
604
921
  const agentes = mensajeDeAgentes();
922
+ // Y las skills, por lo mismo que los subagentes y con una razón más: el editor de un
923
+ // subagente pinta sus skills como una lista de casillas, así que sin esto ese formulario
924
+ // se abriría sin ninguna casilla que marcar — que es indistinguible de «no hay ninguna».
925
+ const skills = mensajeDeSkills();
605
926
  // La cola de tareas, si esta ejecución las tiene: la misma regla que `agentes`, sin
606
927
  // esto la pestaña de tareas se quedaría vacía hasta el primer cambio de la cola.
607
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();
608
932
  /**
609
933
  * Lo consumido por la sesión que se va a pintar. Va en la ráfaga por lo mismo que los
610
934
  * modelos: una pestaña que conecta a mitad de sesión no vio los cambios anteriores, y
@@ -619,8 +943,11 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
619
943
  cliente({ clase: "reemision", actos: [...actos] });
620
944
  cliente(modelos);
621
945
  cliente(agentes);
946
+ cliente(skills);
622
947
  if (tareas !== undefined)
623
948
  cliente(tareas);
949
+ if (workspace !== undefined)
950
+ cliente(workspace);
624
951
  if (consumoDeLaSesion !== undefined) {
625
952
  cliente({
626
953
  clase: "consumo",
@@ -708,20 +1035,33 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
708
1035
  * heredar un «ya se intentó» que se quedó en `proyectos: []` para siempre. Esto puede
709
1036
  * abrir el navegador de verdad si el token necesita reautenticar —`conectarCloudStudio`
710
1037
  * ya lo hace así—, y es lo correcto: un token vivo no toca el puerto de callback en
711
- * absoluto (arreglado en `agent/cloudstudioMcp.ts#abrirCliente`), así que dos conexiones
1038
+ * absoluto (arreglado en `agent/cloudstudio/cloudstudioMcp.ts#abrirCliente`), así que dos conexiones
712
1039
  * seguidas no chocan por intentarlo cada una.
713
1040
  */
714
- const poblarProyectosSiProcede = async () => {
1041
+ /**
1042
+ * De qué entorno se poblarían los proyectos al arrancar, o nada si no hay nada que
1043
+ * preparar.
1044
+ *
1045
+ * Es la MISMA decisión que toma `poblarProyectosSiProcede`, extraída para que el arranque
1046
+ * pueda anunciar la fase ANTES de empezarla. En dos sitios divergirían, y el precio sería
1047
+ * de los peores: el lienzo esperando por un trabajo que nadie va a pedir (`preparando`
1048
+ * puesto y nada que lo quite), o entrar en vacío justo cuando sí había algo que traer.
1049
+ */
1050
+ const entornoAPoblar = () => {
715
1051
  if (vestibulo.proyectoAbierto() !== undefined)
716
- return;
1052
+ return undefined;
717
1053
  if (entornoElegido !== undefined)
718
- return;
1054
+ return undefined;
719
1055
  const [primero] = vestibulo.entornosRegistrados();
720
- if (primero === undefined)
1056
+ return primero === undefined ? undefined : { id: primero.id, nombre: primero.nombre };
1057
+ };
1058
+ const poblarProyectosSiProcede = async () => {
1059
+ const destino = entornoAPoblar();
1060
+ if (destino === undefined)
721
1061
  return;
722
1062
  try {
723
- proyectos = await vestibulo.proyectosDe(primero.id);
724
- entornoElegido = primero.id;
1063
+ proyectos = await vestibulo.proyectosDe(destino.id);
1064
+ entornoElegido = destino.id;
725
1065
  }
726
1066
  catch (error) {
727
1067
  aviso = error instanceof Error ? error.message : String(error);
@@ -746,6 +1086,21 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
746
1086
  });
747
1087
  /** Los cuatro nombres conocidos y nada más: lo que llega por el cable no elige binario. */
748
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
+ };
749
1104
  const mensajeDeTareas = () => {
750
1105
  if (opciones.colaDeTareas === undefined)
751
1106
  return undefined;
@@ -1033,7 +1388,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1033
1388
  /**
1034
1389
  * «Se edita la tarea y se agrega el feedback del usuario» (§0 del diseño, textual):
1035
1390
  * añadir un feedback a una tarea «esperando feedback» la devuelve al lazo. Vive detrás de
1036
- * `aplicarFeedback` (`agent/tareasEnDisco.ts`) y no repite su lógica aquí, por el mismo
1391
+ * `aplicarFeedback` (`agent/tareas/tareasEnDisco.ts`) y no repite su lógica aquí, por el mismo
1037
1392
  * motivo que `atenderCrearTarea`/`atenderAccionDeTarea` no reimplementan `conEstado`: la
1038
1393
  * regla de qué feedback vale y qué transición es legal está en una sola función, probada
1039
1394
  * sola y sin necesitar un servidor de mentira alrededor.
@@ -1051,6 +1406,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1051
1406
  /** Los ajustes de destino, o `{}` —que es «se miran todos»— si esta ejecución no los lee. */
1052
1407
  const ajustesDeDispositivos = () => opciones.ajustesDeDispositivos?.() ?? {};
1053
1408
  let deteccionEnVuelo;
1409
+ /**
1410
+ * Cómo acabó el último «Arrancar». Viaja con la foto porque el resultado de arrancar ES la
1411
+ * foto: `emulator` devuelve el control enseguida y lo que importa es si el aparato apareció.
1412
+ * Se limpia al pedir una medida sin arranque de por medio, para no repetir un acuse viejo.
1413
+ */
1414
+ let ultimoArranque;
1054
1415
  const atenderDispositivos = () => {
1055
1416
  if (opciones.detectarDispositivos === undefined)
1056
1417
  return Promise.resolve();
@@ -1060,7 +1421,12 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1060
1421
  deteccionEnVuelo = (async () => {
1061
1422
  try {
1062
1423
  informeDeDispositivos = sinRutas(await detectar());
1063
- emitir({ clase: "dispositivos", informe: informeDeDispositivos, ajustes: ajustesDeDispositivos() });
1424
+ emitir({
1425
+ clase: "dispositivos",
1426
+ informe: informeDeDispositivos,
1427
+ ajustes: ajustesDeDispositivos(),
1428
+ ...(ultimoArranque === undefined ? {} : { arranque: ultimoArranque }),
1429
+ });
1064
1430
  }
1065
1431
  catch (error) {
1066
1432
  // El detector reparte los fallos por herramienta y no lanza por ninguno; que
@@ -1075,6 +1441,55 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1075
1441
  })();
1076
1442
  return deteccionEnVuelo;
1077
1443
  };
1444
+ /**
1445
+ * **Arrancar un AVD, y contarlo con la foto de después.**
1446
+ *
1447
+ * Tres guardas, en este orden:
1448
+ *
1449
+ * 1. **Que esta ejecución pueda.** Sin el puerto no hay arranque y se DICE: un botón que
1450
+ * llega a un servidor que no sabe arrancar nada es el control muerto de siempre.
1451
+ * 2. **Que ese AVD esté en NUESTRA última medida.** El nombre llega por el cable y acaba
1452
+ * siendo un argumento de proceso; la lista de AVDs es lo único que lo autoriza. Sin
1453
+ * medida todavía no se arranca nada — no hay con qué comprobarlo.
1454
+ * 3. **Uno a la vez.** Dos arranques del mismo AVD a la vez son dos emuladores peleándose
1455
+ * por el mismo puerto, y de otro AVD sería trabajo pesado en paralelo en la máquina de
1456
+ * quien mira. Se reenvía el estado, que es lo que hace la receta.
1457
+ *
1458
+ * Al acabar se vuelve a medir SIEMPRE, salga bien o mal: la foto es la que dice si arrancó.
1459
+ */
1460
+ let arranqueEnVuelo;
1461
+ const atenderArranqueDeEmulador = async (avd) => {
1462
+ const arrancar = opciones.arrancarEmulador;
1463
+ if (arrancar === undefined) {
1464
+ informar("esta ejecución no puede arrancar emuladores");
1465
+ return;
1466
+ }
1467
+ if (arranqueEnVuelo !== undefined) {
1468
+ informar(`ya estoy arrancando ${arranqueEnVuelo}`);
1469
+ return;
1470
+ }
1471
+ if (informeDeDispositivos === undefined || !informeDeDispositivos.avds.includes(avd)) {
1472
+ ultimoArranque = { avd, ok: false, detalle: `no consta ningún AVD llamado «${avd}» en la última medida` };
1473
+ await atenderDispositivos().catch(contar);
1474
+ return;
1475
+ }
1476
+ arranqueEnVuelo = avd;
1477
+ try {
1478
+ const r = await arrancar(avd);
1479
+ ultimoArranque = { avd, ...r };
1480
+ }
1481
+ catch (error) {
1482
+ // El puerto promete no lanzar; si lanzara, el botón no puede quedarse mudo.
1483
+ ultimoArranque = { avd, ok: false, detalle: error instanceof Error ? error.message : String(error) };
1484
+ contar(error);
1485
+ }
1486
+ finally {
1487
+ arranqueEnVuelo = undefined;
1488
+ // La medida nueva, pase lo que pase: es la que dice la verdad.
1489
+ informeDeDispositivos = undefined;
1490
+ await atenderDispositivos().catch(contar);
1491
+ }
1492
+ };
1078
1493
  /**
1079
1494
  * Verificar la conexión con UN dispositivo, y volver a emitir la foto con lo que contestó.
1080
1495
  *
@@ -1125,11 +1540,32 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1125
1540
  }
1126
1541
  try {
1127
1542
  const modelos = await opciones.catalogoDeModelos(id);
1128
- 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
+ });
1129
1565
  }
1130
1566
  catch (error) {
1131
1567
  // `ErrorCatalogoModelos` es publicable por contrato: nunca lleva la clave ni el
1132
- // cuerpo remoto (`agent/catalogoModelos.ts`).
1568
+ // cuerpo remoto (`agent/config/catalogoModelos.ts`).
1133
1569
  catalogos.set(id, { error: error instanceof Error ? error.message : String(error) });
1134
1570
  }
1135
1571
  emitirModelos();
@@ -1470,6 +1906,31 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1470
1906
  // El cable se muda a la consola del proyecto, como en el alta: sin esto el usuario
1471
1907
  // mira un transcript vivo cuyas aprobaciones se rechazan solas al otro lado.
1472
1908
  adjuntar();
1909
+ /**
1910
+ * Y el contador se REEMITE, porque el gasto es de UNA conversación.
1911
+ *
1912
+ * `consumo` solo viaja cuando el consumo CAMBIA (`alCambiarConsumo`), y abrir otra
1913
+ * sesión no cambia nada: cambia de quién es la cuenta. Sin esto —medido en la pantalla
1914
+ * del usuario— la caja seguía enseñando los tokens de la conversación anterior hasta la
1915
+ * primera llamada al modelo de la nueva, que puede tardar.
1916
+ *
1917
+ * **No se usa `emitirConsumo()`, y ese es el detalle que costó el primer intento**: esa
1918
+ * función CALLA cuando no consta la cuenta, y al abrir no consta — el `SesionReal` no se
1919
+ * registra hasta que el primer turno lo construye. O sea que justo en el instante que
1920
+ * hay que limpiar, la vía normal no manda nada. Aquí el cero SÍ está medido: una
1921
+ * conversación recién abierta no ha gastado nada, y su tracker nace a cero aunque la
1922
+ * sesión sea vieja. El cliente no pinta un cero, así que el contador desaparece.
1923
+ *
1924
+ * Y si la cuenta SÍ consta —volver al foco de una consola que está corriendo un turno—
1925
+ * se manda la de verdad: mandar cero ahí borraría un contador vivo a mitad de turno.
1926
+ */
1927
+ const alAbrir = vestibulo.consumoDeSesion();
1928
+ emitir({
1929
+ clase: "consumo",
1930
+ modelo: alAbrir?.modelo ?? SIN_CONSUMO,
1931
+ externo: alAbrir?.externo ?? SIN_CONSUMO,
1932
+ ventana: ventanaDeAhora(alAbrir?.contexto ?? 0),
1933
+ });
1473
1934
  // Y la cola se vuelve a mirar: este proyecto queda bloqueado para las tareas —gana la
1474
1935
  // persona— y el que estuviera abierto antes acaba de quedar libre.
1475
1936
  opciones.revisarTareas?.();
@@ -1526,6 +1987,72 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1526
1987
  * Si no hay escritor, se DICE que no se ha guardado en vez de callarlo: un ajuste que
1527
1988
  * parece puesto y no lo está es peor que uno que falta.
1528
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
+ };
1529
2056
  const atenderModelo = (id) => {
1530
2057
  try {
1531
2058
  parsear(id);
@@ -1761,7 +2288,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1761
2288
  };
1762
2289
  /**
1763
2290
  * La sincronización con CloudStudio del proyecto abierto, pedida desde un control
1764
- * (la banda de arriba de Revisión, y «Volver a mirar»).
2291
+ * (la banda de arriba de Revisión, y «Refrescar»).
1765
2292
  *
1766
2293
  * **`estado` se mide aquí; `subir` y `bajar` se ENCOLAN**, y la asimetría es deliberada.
1767
2294
  * Medir es leer una ref de git que ya está en local (`lecturaDeSync`), así que encolarla
@@ -1778,7 +2305,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1778
2305
  * Y lo que NO se hace es volver a emitir la lectura después de encolar: el servidor no
1779
2306
  * sabe cuándo termina una línea de la cola, y un «3 ficheros por subir» recién emitido
1780
2307
  * tras pulsar Subir sería una cifra que nadie ha vuelto a medir. La pestaña se refresca
1781
- * al volver a ella y al pulsar «Volver a mirar», que es cuando de verdad se mira.
2308
+ * al volver a ella y al pulsar «Refrescar», que es cuando de verdad se mira.
1782
2309
  */
1783
2310
  const atenderSync = (accion) => {
1784
2311
  const abierto = vestibulo.proyectoAbierto();
@@ -2512,10 +3039,48 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2512
3039
  // documenta la medida).
2513
3040
  // Solo al recién llegado: los demás ya recibieron su saludo al conectar.
2514
3041
  sumidero({ clase: "bienvenida", ...(vestibulo.nombre === undefined ? {} : { nombre: vestibulo.nombre }) });
3042
+ /**
3043
+ * **Dos anuncios, y el primero va ANTES de preguntar a CloudStudio.**
3044
+ *
3045
+ * `pasos` se calcula del DISCO (`pasosPendientes`: de dónde vino el modelo y qué entornos
3046
+ * hay registrados), así que en una máquina configurada ya se sabe «no falta nada» sin
3047
+ * tocar la red. Con un solo anuncio detrás de `poblarProyectosSiProcede` —que abre la
3048
+ * sesión MCP— el cliente no podía saberlo hasta que ese viaje terminara, y `App.tsx` lee
3049
+ * un `alta` ausente como «todavía no consta»: enseñaba la pantalla del alta a alguien que
3050
+ * no tiene nada que dar de alta. Medido en la máquina del usuario, con CloudStudio
3051
+ * caliente: el `alta` llegaba a 1436 ms, y con el MCP lento o la red caída esa ventana
3052
+ * crece hasta su tope. Es la misma forma que `MS_DE_TRABAJO_AL_ABRIR`: un dato que se
3053
+ * sabe y no se dice porque va detrás de otro que tarda.
3054
+ *
3055
+ * El segundo anuncio lleva los proyectos, y sigue en un `finally` por lo de siempre: que
3056
+ * `poblarProyectosSiProcede` acabe mal no puede dejar al cliente sin el anuncio. Es el
3057
+ * mismo reparto que ya usaba el cambio de entorno (anunciar, luego preguntar).
3058
+ */
3059
+ /**
3060
+ * La fase se pone ANTES del primer anuncio y se quita en el `finally`, así que el primer
3061
+ * `alta` sale con ella y el segundo sin ella. Es lo que hace que el Escritorio no entre
3062
+ * hasta estar preparado —lo que el usuario pidió— sin volver a esconder el «no falta
3063
+ * ningún paso», que es lo que el `alta` adelantado vino a arreglar: el cliente sabe
3064
+ * desde el primer mensaje que no hay nada que dar de alta Y que todavía está trayendo
3065
+ * los proyectos, que son dos cosas distintas.
3066
+ *
3067
+ * Con la cuenta pendiente y un entorno ya registrado las dos cosas pasan a la vez, y eso
3068
+ * está bien: la tarjeta pinta el paso de cuenta mientras el MCP trabaja por detrás. Es el
3069
+ * cliente quien decide qué se ve, porque la fase es un dato, no una orden de pintar.
3070
+ */
3071
+ const aPreparar = entornoAPoblar();
3072
+ preparando = aPreparar === undefined ? undefined : `conectando con ${aPreparar.nombre}…`;
2515
3073
  void conducirCuenta()
2516
3074
  .catch(contar)
2517
- .then(() => poblarProyectosSiProcede())
2518
- .finally(() => void anunciarAlta().catch(contar));
3075
+ .then(() => anunciarAlta().catch(contar))
3076
+ .then(() => conPlazo(poblarProyectosSiProcede(), MS_DE_PREPARACION))
3077
+ .finally(() => {
3078
+ // Se quita SIEMPRE, también cuando el plazo vence con el MCP todavía colgado: el
3079
+ // lienzo no puede quedarse esperando a quien no contesta. Lo que llegue tarde entra
3080
+ // en el siguiente anuncio, y lo que falló ya viaja en `aviso`.
3081
+ preparando = undefined;
3082
+ void anunciarAlta().catch(contar);
3083
+ });
2519
3084
  peticion.on("close", () => {
2520
3085
  // Se va ESTE cliente, no «el cliente». La guarda de antes (`enviar !== sumidero`)
2521
3086
  // existía porque el `close` de una pestaña recargada puede llegar DESPUÉS de que el
@@ -2675,6 +3240,61 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2675
3240
  * - **Ninguna respuesta lleva una ruta de la máquina** ni nada de lo recibido, que es la
2676
3241
  * regla de `POST /accion`.
2677
3242
  */
3243
+ /**
3244
+ * `POST /skill?nombre=<fichero.zip>&ambito=<global|proyecto>` — instalar una skill.
3245
+ *
3246
+ * Por HTTP y no por el cable, con el mismo molde que `POST /adjunto`: esto son bytes y el
3247
+ * cable lleva JSON. `Host`, `Origin` y token los comprueba `servidor.ts` antes de llegar
3248
+ * aquí, igual que a todas las rutas. Lo propio de ésta:
3249
+ *
3250
+ * - **El `nombre` de la query es el del FICHERO que subió la persona, y solo se usa como
3251
+ * nombre de respaldo** cuando el zip no trae una carpeta de la que sacarlo
3252
+ * (`core/zipDeSkill.ts`). No se compone ninguna ruta con él sin pasar por la regla del
3253
+ * slug, que es la misma de un subagente.
3254
+ * - **El cuerpo se lee con el tope del ZIP y se corta ahí** (413), sin acumular. Y el otro
3255
+ * tope, el de lo DESCOMPRIMIDO, lo pone `planDeInstalacion`: éste no lo ve, que es
3256
+ * justamente lo que un zip bomba aprovecha.
3257
+ * - **Ninguna respuesta lleva una ruta de la máquina ni nada de lo recibido**, que es la
3258
+ * regla de `POST /accion` — y aquí lo recibido es un fichero de alguien.
3259
+ */
3260
+ servidor.registrarRuta("POST", RUTA_SKILL, async (peticion, respuesta) => {
3261
+ const responder = (codigo, texto) => {
3262
+ respuesta.writeHead(codigo, { "Content-Type": "text/plain; charset=utf-8" });
3263
+ respuesta.end(texto);
3264
+ };
3265
+ const query = new URLSearchParams((peticion.url ?? "").split("?")[1] ?? "");
3266
+ const nombre = query.get("nombre");
3267
+ const ambito = query.get("ambito") === "proyecto" ? "proyecto" : "global";
3268
+ if (nombre === null || nombre === "") {
3269
+ responder(400, "falta «nombre»");
3270
+ return;
3271
+ }
3272
+ const abierto = vestibulo.proyectoAbierto();
3273
+ if (ambito === "proyecto" && abierto === undefined) {
3274
+ responder(409, "no hay ningún proyecto abierto: esa skill solo se puede instalar como global");
3275
+ return;
3276
+ }
3277
+ let datos;
3278
+ try {
3279
+ datos = await leerCuerpoCrudo(peticion, TOPE_DEL_ZIP);
3280
+ }
3281
+ catch {
3282
+ responder(413, "el .zip es demasiado grande");
3283
+ return;
3284
+ }
3285
+ const instalada = instalarSkillDesdeZip(ambito === "proyecto" ? abierto.raiz : homedir(), new Uint8Array(datos), nombre);
3286
+ if ("error" in instalada) {
3287
+ // El motivo lo escribe el módulo y no lleva ninguna ruta de la máquina ni el nombre de
3288
+ // una entrada del zip (sus tests lo vigilan). 422: el zip llegó entero y no vale.
3289
+ informar(`no se instala la skill: ${instalada.error}`);
3290
+ responder(422, instalada.error);
3291
+ return;
3292
+ }
3293
+ informar(`skill «${instalada.nombre}» instalada en ${ambito}`);
3294
+ emitirSkills();
3295
+ respuesta.writeHead(204);
3296
+ respuesta.end();
3297
+ });
2678
3298
  servidor.registrarRuta("POST", RUTA_ADJUNTO, async (peticion, respuesta) => {
2679
3299
  const responder = (codigo, texto) => {
2680
3300
  respuesta.writeHead(codigo, { "Content-Type": "text/plain; charset=utf-8" });
@@ -2808,6 +3428,17 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2808
3428
  respuesta.end();
2809
3429
  return;
2810
3430
  }
3431
+ if (typeof mensaje === "object" &&
3432
+ mensaje !== null &&
3433
+ mensaje.clase === "arrancarEmulador" &&
3434
+ typeof mensaje.avd === "string") {
3435
+ // Se contesta ya: arrancar tarda lo que tarda un emulador y la respuesta del POST no es
3436
+ // el sitio donde se cuenta — eso viaja por el SSE con la foto nueva.
3437
+ void atenderArranqueDeEmulador(mensaje.avd).catch(contar);
3438
+ respuesta.writeHead(204);
3439
+ respuesta.end();
3440
+ return;
3441
+ }
2811
3442
  /**
2812
3443
  * ANTES del `recibir` de la consola, y también antes que las demás clases de tarea: por
2813
3444
  * aquí no entra nada hacia ningún turno. Ver `atenderMirar`.
@@ -2875,6 +3506,40 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2875
3506
  respuesta.end();
2876
3507
  return;
2877
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
+ }
2878
3543
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "modelosDeMotor" && typeof mensaje.motor === "string") {
2879
3544
  void atenderModelosDeMotor(mensaje.motor).catch(contar);
2880
3545
  respuesta.writeHead(204);
@@ -2912,12 +3577,30 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2912
3577
  respuesta.end();
2913
3578
  return;
2914
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
+ }
2915
3586
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "agente") {
2916
3587
  atenderAgente(mensaje);
2917
3588
  respuesta.writeHead(204);
2918
3589
  respuesta.end();
2919
3590
  return;
2920
3591
  }
3592
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "skill") {
3593
+ atenderSkill(mensaje);
3594
+ respuesta.writeHead(204);
3595
+ respuesta.end();
3596
+ return;
3597
+ }
3598
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "cuerpoDeSkill") {
3599
+ atenderCuerpoDeSkill(mensaje);
3600
+ respuesta.writeHead(204);
3601
+ respuesta.end();
3602
+ return;
3603
+ }
2921
3604
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "proveedor") {
2922
3605
  atenderProveedor(mensaje);
2923
3606
  respuesta.writeHead(204);
@@ -3034,6 +3717,19 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3034
3717
  respuesta.end();
3035
3718
  return;
3036
3719
  }
3720
+ /**
3721
+ * **Y el acuse del último arranque se OLVIDA.**
3722
+ *
3723
+ * Sin esto se queda pegado para siempre: el usuario vio `pixel8 · apagado` con un
3724
+ * «✓ pixel8 arrancado» al lado, o sea las dos cosas a la vez. El acuse pertenece a la
3725
+ * foto que se tomó justo después de arrancar, no a todas las de después — y ahora que
3726
+ * la sección remide al entrar y al volver, «todas las de después» son muchas.
3727
+ *
3728
+ * Ausente vuelve a significar «no se ha pedido ningún arranque en esta medida», que es
3729
+ * lo que el campo declara. El `finally` de `atenderArranqueDeEmulador` lo pone y se
3730
+ * salta este camino, así que su propia medida sí lo lleva.
3731
+ */
3732
+ ultimoArranque = undefined;
3037
3733
  informeDeDispositivos = undefined;
3038
3734
  void atenderDispositivos().catch(contar);
3039
3735
  respuesta.writeHead(204);
@@ -3080,7 +3776,7 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3080
3776
  * Construye el corredor de tareas y la costura que lo conecta al cable.
3081
3777
  *
3082
3778
  * Extraído de `arrancarConsolaWeb` a su propia función por la MISMA razón que
3083
- * `backendDeAgente` (`agent/proyecto.ts`): la composición vivía inline dentro de una
3779
+ * `backendDeAgente` (`agent/grafo/proyecto.ts`): la composición vivía inline dentro de una
3084
3780
  * función que todos sus tests DOBLAN —`vestibulo`, `crearServidor`, `crearEjecutor`—, así
3085
3781
  * que esta costura concreta podía dejar de estar montada con el resto en verde. Aquí
3086
3782
  * tiene su propio test que la compone de VERDAD: construye un corredor real
@@ -3362,12 +4058,71 @@ export function augmentacionCableada(opciones) {
3362
4058
  export function commitDeTurnoCableado(opciones) {
3363
4059
  const commitear = opciones.commitear ?? commitDeTurno;
3364
4060
  return async (raiz, mensaje, sesion) => {
3365
- if (!dentroDelWorkspace(raiz, opciones.base))
4061
+ if (!dentroDelWorkspace(raiz, opciones.base()))
3366
4062
  return undefined;
3367
4063
  const hecho = await commitear(raiz, mensaje, sesion);
3368
4064
  return hecho.via === "fallo" ? `no se pudo commitear el turno: ${hecho.motivo}` : undefined;
3369
4065
  };
3370
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
+ }
3371
4126
  export function construirCorredorDeTareasCableado(opciones) {
3372
4127
  const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
3373
4128
  /**
@@ -3561,6 +4316,17 @@ export async function arrancarConsolaWeb(opciones) {
3561
4316
  if (offline && opciones.guion !== true) {
3562
4317
  escribir("este directorio es un proyecto offline: ábrelo con «xonecode --cli»\n");
3563
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 });
3564
4330
  const arrancar = opciones.crearServidor ?? arrancarServidor;
3565
4331
  const servidor = await arrancar({
3566
4332
  puerto: opciones.puerto,
@@ -3664,6 +4430,15 @@ export async function arrancarConsolaWeb(opciones) {
3664
4430
  // la raíz que la tarea necesita para poder correr. Ver el comentario de
3665
4431
  // `OpcionesDeMontaje.colaDeTareas`.
3666
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
+ : {}),
3667
4442
  // Los dos puertos del selector de modelos, con las piezas reales: quién tiene
3668
4443
  // credencial (`auth.json` o el entorno, leído desde el cwd) y el catálogo VIVO.
3669
4444
  hayCredencial: (proveedor) => hayCredencial(proveedor, opciones.cwd),
@@ -3673,7 +4448,7 @@ export async function arrancarConsolaWeb(opciones) {
3673
4448
  borrarCredencial,
3674
4449
  guardarCredencial,
3675
4450
  /**
3676
- * El defecto se GUARDA en el `config.json` global (`agent/configEnDisco.ts#guardarModeloGlobal`),
4451
+ * El defecto se GUARDA en el `config.json` global (`agent/config/configEnDisco.ts#guardarModeloGlobal`),
3677
4452
  * que es el último escalón de la precedencia y el único que sobrevive al proceso. Se
3678
4453
  * escribe para los TRES papeles porque elegir un modelo en la interfaz es una frase
3679
4454
  * sobre el producto y no sobre un papel — el mismo criterio que `/modelo`, que fija los
@@ -3699,7 +4474,7 @@ export async function arrancarConsolaWeb(opciones) {
3699
4474
  cambiosDeSesion,
3700
4475
  parcheDeSesion,
3701
4476
  // El proyecto tal como lo ve el agente, para la pestaña Ficheros: mismo filtro, misma
3702
- // barrera de rutas (`agent/arbolDeProyecto.ts`).
4477
+ // barrera de rutas (`agent/grafo/arbolDeProyecto.ts`).
3703
4478
  arbolDelProyecto: async (raiz) => arbolDeProyecto(raiz),
3704
4479
  leerFichero: leerFicheroDeProyecto,
3705
4480
  leerArtefacto: leerArtefactoDeSesion,
@@ -3714,6 +4489,11 @@ export async function arrancarConsolaWeb(opciones) {
3714
4489
  guardarAjustesDeDispositivos: (ajustes) => void guardarDispositivos(undefined, ajustes),
3715
4490
  instalarHerramienta: instalarHerramientaDeDispositivos,
3716
4491
  verificarDispositivo,
4492
+ // Con la función REAL, y no declarada y sin pasar: es la trampa que este repo ha pagado
4493
+ // nueve veces, y la que el comentario de justo abajo describe para las cuatro del
4494
+ // lanzamiento. `montarRutas` lo trata como ausente si falta, así que olvidarlo aquí
4495
+ // dejaría el botón diciendo «esta ejecución no puede arrancar emuladores» con todo verde.
4496
+ arrancarEmulador: (avd) => arrancarEmulador(avd),
3717
4497
  /**
3718
4498
  * Las cuatro de «¿se puede lanzar y lánzalo?», compuestas con las funciones REALES.
3719
4499
  *
@@ -3743,6 +4523,14 @@ export async function arrancarConsolaWeb(opciones) {
3743
4523
  const modelos = await new CatalogoModelos(undefined, undefined, proveedoresPersonalizados).listar(proveedor);
3744
4524
  return modelos.map((m) => ({ id: m.id, ...(m.nombre === undefined ? {} : { nombre: m.nombre }) }));
3745
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,
3746
4534
  // Se releen de disco en cada consulta, igual que los ajustes de dispositivos: esta
3747
4535
  // misma ventana los da de alta, y una lista capturada al montar las rutas se quedaría
3748
4536
  // vieja hasta reiniciar el proceso.
@@ -3750,7 +4538,7 @@ export async function arrancarConsolaWeb(opciones) {
3750
4538
  guardarProveedor: (declarado) => guardarProveedorPersonalizado(declarado),
3751
4539
  borrarProveedor: (slug) => borrarProveedorPersonalizado(slug),
3752
4540
  /**
3753
- * El AUMENTADOR de la ventana de crear una tarea (`agent/aumentador.ts`), con el papel
4541
+ * El AUMENTADOR de la ventana de crear una tarea (`agent/tareas/aumentador.ts`), con el papel
3754
4542
  * `trabajo` — es una tarea de redacción, no una clasificación.
3755
4543
  *
3756
4544
  * Tres cosas que se deciden aquí:
@@ -3885,7 +4673,7 @@ function vestibuloReal(opciones, servidor, escribir, informar) {
3885
4673
  // Se resuelve UNA vez, aquí, y no en cada `anunciarAlta`: `git config`/`os.userInfo`
3886
4674
  // no cambian a media conexión, y repetir el subproceso en cada anuncio del alta sería
3887
4675
  // gastar sin motivo. Nunca viaja hacia CloudStudio ni hacia ningún acto —
3888
- // `agent/persona.ts` lo documenta—.
4676
+ // `agent/config/persona.ts` lo documenta—.
3889
4677
  nombre: nombreDePersona(opciones.cwd),
3890
4678
  catalogoModelos: new CatalogoModelos(),
3891
4679
  guardarCredencial,
@@ -3898,7 +4686,7 @@ function vestibuloReal(opciones, servidor, escribir, informar) {
3898
4686
  guardarModeloGlobal,
3899
4687
  guardarConfigDeProyecto: escribirProyectoEnDisco,
3900
4688
  // El «antes» de cada sesión: se fotografía al abrir el proyecto y se nombra cuando la
3901
- // sesión tiene id. Ver `agent/sesionGit.ts` para por qué es una ref y no un tag.
4689
+ // sesión tiene id. Ver `agent/sesiones/sesionGit.ts` para por qué es una ref y no un tag.
3902
4690
  marcarSesion: fotoDeApertura,
3903
4691
  // Lo que ya había sin commitear al abrir, para poder decirlo. Comparte el hueco
3904
4692
  // declarado de `marcarSesion`: esta composición vive en un cierre que `vestibuloReal`
@@ -3909,14 +4697,25 @@ function vestibuloReal(opciones, servidor, escribir, informar) {
3909
4697
  // El commit del turno, con el sello de la sesión. La composición está EXTRAÍDA y probada
3910
4698
  // (`commitDeTurnoCableado`): el argumento que se cae en una lambda escrita a mano es
3911
4699
  // justo el que sostiene la atribución de Revisión.
3912
- commitearTurno: commitDeTurnoCableado({ base: settings.workspace ?? baseDeWorkspacePorOmision() }),
4700
+ commitearTurno: commitDeTurnoCableado({
4701
+ base: () => cargarSettings().settings.workspace ?? baseDeWorkspacePorOmision(),
4702
+ }),
3913
4703
  olvidarMarcaDeSesion: olvidarSesion,
3914
4704
  // La memoria del agente por hilo. `historica` deja de ser «se reabrió» para ser «no hay
3915
4705
  // checkpoint que cargar», y borrar una sesión se lleva también su checkpoint.
3916
4706
  hayMemoriaDeHilo: async (raiz, hilo) => hayCheckpoint(crearCheckpointerDeProyecto(raiz), hilo),
3917
4707
  olvidarMemoriaDeHilo: async (raiz, hilo) => olvidarHilo(crearCheckpointerDeProyecto(raiz), hilo),
3918
4708
  entornos: settings.entornos,
3919
- ...(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(),
3920
4719
  // La URL de la web para que la página del callback devuelva AQUÍ y no diga «vuelve a
3921
4720
  // la terminal», que en un navegador es falso.
3922
4721
  ...conexionDeVestibulo(servidor.url),