xonecode 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +50 -8
  2. package/THIRD_PARTY_NOTICES.md +160 -0
  3. package/apps/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/apps/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/apps/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/apps/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/apps/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/apps/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/apps/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/apps/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/apps/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/apps/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/apps/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/apps/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/apps/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/apps/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/apps/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/apps/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/apps/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/apps/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/apps/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/apps/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/apps/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/apps/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/apps/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/apps/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/apps/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/apps/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/apps/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/apps/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/apps/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/apps/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/apps/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/apps/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/apps/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/apps/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/apps/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/apps/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/apps/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/apps/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/apps/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/apps/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/apps/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/apps/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/apps/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/apps/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/apps/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/apps/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/apps/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/apps/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/apps/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/apps/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/apps/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/apps/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/apps/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/apps/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/apps/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/apps/web/dist/assets/c-BIGW1oBm.js +1 -0
  63. package/apps/web/dist/assets/cpp-DddhVWhK.js +1 -0
  64. package/apps/web/dist/assets/csharp-DSvCPggb.js +1 -0
  65. package/apps/web/dist/assets/css-CLj8gQPS.js +1 -0
  66. package/apps/web/dist/assets/go-C27-OAKa.js +1 -0
  67. package/apps/web/dist/assets/html-CfGypltT.js +1 -0
  68. package/apps/web/dist/assets/index-BB3SamhR.css +1 -0
  69. package/apps/web/dist/assets/index-CFYAoHdw.js +477 -0
  70. package/apps/web/dist/assets/ini-BEwlwnbL.js +1 -0
  71. package/apps/web/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  72. package/apps/web/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  73. package/apps/web/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  74. package/apps/web/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  75. package/apps/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  76. package/apps/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  77. package/apps/web/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  78. package/apps/web/dist/assets/java-CylS5w8V.js +1 -0
  79. package/apps/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  80. package/apps/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  81. package/apps/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  82. package/apps/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  83. package/apps/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  84. package/apps/web/dist/assets/kotlin-BdnUsdx6.js +1 -0
  85. package/apps/web/dist/assets/less-B1dDrJ26.js +1 -0
  86. package/apps/web/dist/assets/lua-BaeVxFsk.js +1 -0
  87. package/apps/web/dist/assets/markdown-Cvjx9yec.js +1 -0
  88. package/apps/web/dist/assets/mdx-Cmh6b_Ma.js +1 -0
  89. package/apps/web/dist/assets/php-D_2--9WK.js +1 -0
  90. package/apps/web/dist/assets/python-B6aJPvgy.js +1 -0
  91. package/apps/web/dist/assets/ruby-DSHSDbk1.js +1 -0
  92. package/apps/web/dist/assets/rust-B1yitclQ.js +1 -0
  93. package/apps/web/dist/assets/scss-D5BDwBP9.js +1 -0
  94. package/apps/web/dist/assets/sql-CRqJ_cUM.js +1 -0
  95. package/apps/web/dist/assets/swift-C2oV4EkX.js +1 -0
  96. package/apps/web/dist/assets/toml-vGWfd6FD.js +1 -0
  97. package/apps/web/dist/assets/xml-sdJ4AIDG.js +1 -0
  98. package/apps/web/dist/assets/yaml-Buea-lGh.js +1 -0
  99. package/apps/web/dist/iconos/xonecode.png +0 -0
  100. package/apps/web/dist/index.html +20 -0
  101. package/dist/agent/agentesEnDisco.js +539 -0
  102. package/dist/agent/agentesEnDisco.js.map +1 -0
  103. package/dist/agent/arbolDeProyecto.js +245 -0
  104. package/dist/agent/arbolDeProyecto.js.map +1 -0
  105. package/dist/agent/artefactosEnDisco.js +139 -0
  106. package/dist/agent/artefactosEnDisco.js.map +1 -0
  107. package/dist/agent/aumentador.js +175 -0
  108. package/dist/agent/aumentador.js.map +1 -0
  109. package/dist/agent/authEnDisco.js +76 -1
  110. package/dist/agent/authEnDisco.js.map +1 -1
  111. package/dist/agent/busquedaRegex.js +92 -0
  112. package/dist/agent/busquedaRegex.js.map +1 -0
  113. package/dist/agent/catalogoModelos.js +345 -0
  114. package/dist/agent/catalogoModelos.js.map +1 -0
  115. package/dist/agent/checkpointer.js +127 -0
  116. package/dist/agent/checkpointer.js.map +1 -0
  117. package/dist/agent/cloudstudioClient.js +194 -0
  118. package/dist/agent/cloudstudioClient.js.map +1 -0
  119. package/dist/agent/cloudstudioMcp.js +722 -0
  120. package/dist/agent/cloudstudioMcp.js.map +1 -0
  121. package/dist/agent/configEnDisco.js +309 -11
  122. package/dist/agent/configEnDisco.js.map +1 -1
  123. package/dist/agent/consumoExterno.js +99 -0
  124. package/dist/agent/consumoExterno.js.map +1 -0
  125. package/dist/agent/descarga.js +149 -0
  126. package/dist/agent/descarga.js.map +1 -0
  127. package/dist/agent/diagnosticoDeTools.js +51 -0
  128. package/dist/agent/diagnosticoDeTools.js.map +1 -0
  129. package/dist/agent/dispositivosEnMaquina.js +596 -0
  130. package/dist/agent/dispositivosEnMaquina.js.map +1 -0
  131. package/dist/agent/escrituraDeCodex.js +134 -0
  132. package/dist/agent/escrituraDeCodex.js.map +1 -0
  133. package/dist/agent/escrituraDeOpencode.js +80 -0
  134. package/dist/agent/escrituraDeOpencode.js.map +1 -0
  135. package/dist/agent/escrituraExterna.js +742 -0
  136. package/dist/agent/escrituraExterna.js.map +1 -0
  137. package/dist/agent/gemini.js +81 -0
  138. package/dist/agent/gemini.js.map +1 -0
  139. package/dist/agent/git.js +83 -0
  140. package/dist/agent/git.js.map +1 -0
  141. package/dist/agent/gitSync.js +441 -0
  142. package/dist/agent/gitSync.js.map +1 -0
  143. package/dist/agent/hotswap.js +448 -0
  144. package/dist/agent/hotswap.js.map +1 -0
  145. package/dist/agent/instalacionEnMaquina.js +252 -0
  146. package/dist/agent/instalacionEnMaquina.js.map +1 -0
  147. package/dist/agent/instantanea.js +7 -10
  148. package/dist/agent/instantanea.js.map +1 -1
  149. package/dist/agent/interrupts.js +11 -2
  150. package/dist/agent/interrupts.js.map +1 -1
  151. package/dist/agent/juezDeTarea.js +345 -0
  152. package/dist/agent/juezDeTarea.js.map +1 -0
  153. package/dist/agent/lanzamientoEnMaquina.js +377 -0
  154. package/dist/agent/lanzamientoEnMaquina.js.map +1 -0
  155. package/dist/agent/manifiesto.js +59 -0
  156. package/dist/agent/manifiesto.js.map +1 -0
  157. package/dist/agent/memoriaDeProyecto.js +60 -0
  158. package/dist/agent/memoriaDeProyecto.js.map +1 -0
  159. package/dist/agent/modelos.js +105 -17
  160. package/dist/agent/modelos.js.map +1 -1
  161. package/dist/agent/modelosDeMotor.js +204 -0
  162. package/dist/agent/modelosDeMotor.js.map +1 -0
  163. package/dist/agent/paqueteDelProyecto.js +181 -0
  164. package/dist/agent/paqueteDelProyecto.js.map +1 -0
  165. package/dist/agent/perfiles.js +92 -31
  166. package/dist/agent/perfiles.js.map +1 -1
  167. package/dist/agent/persona.js +40 -0
  168. package/dist/agent/persona.js.map +1 -0
  169. package/dist/agent/procesosEnMaquina.js +204 -0
  170. package/dist/agent/procesosEnMaquina.js.map +1 -0
  171. package/dist/agent/proyecto.js +311 -13
  172. package/dist/agent/proyecto.js.map +1 -1
  173. package/dist/agent/puente.js +63 -6
  174. package/dist/agent/puente.js.map +1 -1
  175. package/dist/agent/resumenDeContexto.js +30 -0
  176. package/dist/agent/resumenDeContexto.js.map +1 -0
  177. package/dist/agent/resumenDeTool.js +64 -18
  178. package/dist/agent/resumenDeTool.js.map +1 -1
  179. package/dist/agent/sesionGit.js +481 -0
  180. package/dist/agent/sesionGit.js.map +1 -0
  181. package/dist/agent/settingsEnDisco.js +189 -0
  182. package/dist/agent/settingsEnDisco.js.map +1 -0
  183. package/dist/agent/subagenteCodex.js +368 -0
  184. package/dist/agent/subagenteCodex.js.map +1 -0
  185. package/dist/agent/subagenteExterno.js +423 -0
  186. package/dist/agent/subagenteExterno.js.map +1 -0
  187. package/dist/agent/subagenteOpencode.js +389 -0
  188. package/dist/agent/subagenteOpencode.js.map +1 -0
  189. package/dist/agent/subida.js +202 -0
  190. package/dist/agent/subida.js.map +1 -0
  191. package/dist/agent/tareasEnDisco.js +466 -0
  192. package/dist/agent/tareasEnDisco.js.map +1 -0
  193. package/dist/agent/turnoReal.js +663 -50
  194. package/dist/agent/turnoReal.js.map +1 -1
  195. package/dist/agent/versionEnDisco.js +48 -0
  196. package/dist/agent/versionEnDisco.js.map +1 -0
  197. package/dist/agent/xoneAgent.js +290 -49
  198. package/dist/agent/xoneAgent.js.map +1 -1
  199. package/dist/agent/zip.js +52 -0
  200. package/dist/agent/zip.js.map +1 -0
  201. package/dist/cli/acuseDeModelo.js +30 -0
  202. package/dist/cli/acuseDeModelo.js.map +1 -0
  203. package/dist/cli/aprobar.js +55 -2
  204. package/dist/cli/aprobar.js.map +1 -1
  205. package/dist/cli/config.js +9 -13
  206. package/dist/cli/config.js.map +1 -1
  207. package/dist/cli/consola.js +750 -19
  208. package/dist/cli/consola.js.map +1 -1
  209. package/dist/cli/main.js +740 -81
  210. package/dist/cli/main.js.map +1 -1
  211. package/dist/cli/markdown.js +188 -2
  212. package/dist/cli/markdown.js.map +1 -1
  213. package/dist/cli/run.js +32 -4
  214. package/dist/cli/run.js.map +1 -1
  215. package/dist/cli/stdio.js +46 -19
  216. package/dist/cli/stdio.js.map +1 -1
  217. package/dist/cli/tema.js +96 -8
  218. package/dist/cli/tema.js.map +1 -1
  219. package/dist/cli/tokens.js +29 -0
  220. package/dist/cli/tokens.js.map +1 -0
  221. package/dist/cli/tui/app.js +171 -0
  222. package/dist/cli/tui/app.js.map +1 -0
  223. package/dist/cli/tui/aprobarTui.js +80 -0
  224. package/dist/cli/tui/aprobarTui.js.map +1 -0
  225. package/dist/cli/tui/barra.js +37 -0
  226. package/dist/cli/tui/barra.js.map +1 -0
  227. package/dist/cli/tui/correrTui.js +478 -0
  228. package/dist/cli/tui/correrTui.js.map +1 -0
  229. package/dist/cli/tui/entrada.js +166 -0
  230. package/dist/cli/tui/entrada.js.map +1 -0
  231. package/dist/cli/tui/filas.js +24 -0
  232. package/dist/cli/tui/filas.js.map +1 -0
  233. package/dist/cli/tui/logo.js +49 -0
  234. package/dist/cli/tui/logo.js.map +1 -0
  235. package/dist/cli/tui/pielTui.js +14 -0
  236. package/dist/cli/tui/pielTui.js.map +1 -0
  237. package/dist/cli/tui/raton.js +126 -0
  238. package/dist/cli/tui/raton.js.map +1 -0
  239. package/dist/cli/tui/sidebar.js +86 -0
  240. package/dist/cli/tui/sidebar.js.map +1 -0
  241. package/dist/cli/tui/store.js +143 -0
  242. package/dist/cli/tui/store.js.map +1 -0
  243. package/dist/cli/tui/tarjeta.js +19 -0
  244. package/dist/cli/tui/tarjeta.js.map +1 -0
  245. package/dist/cli/tui/temaInk.js +114 -0
  246. package/dist/cli/tui/temaInk.js.map +1 -0
  247. package/dist/cli/tui/transcript.js +254 -0
  248. package/dist/cli/tui/transcript.js.map +1 -0
  249. package/dist/cli/wizardInicial.js +240 -0
  250. package/dist/cli/wizardInicial.js.map +1 -0
  251. package/dist/core/actos.js +113 -0
  252. package/dist/core/actos.js.map +1 -0
  253. package/dist/core/adjuntos.js +97 -0
  254. package/dist/core/adjuntos.js.map +1 -0
  255. package/dist/core/agentes.js +227 -0
  256. package/dist/core/agentes.js.map +1 -0
  257. package/dist/core/artefactos.js +149 -0
  258. package/dist/core/artefactos.js.map +1 -0
  259. package/dist/core/cloudstudio.js +7 -0
  260. package/dist/core/cloudstudio.js.map +1 -0
  261. package/dist/core/config.js +212 -5
  262. package/dist/core/config.js.map +1 -1
  263. package/dist/core/contextos.js +53 -1
  264. package/dist/core/contextos.js.map +1 -1
  265. package/dist/core/descargas.js +106 -0
  266. package/dist/core/descargas.js.map +1 -0
  267. package/dist/core/descriptoresDeApp.js +279 -0
  268. package/dist/core/descriptoresDeApp.js.map +1 -0
  269. package/dist/core/dispositivos.js +466 -0
  270. package/dist/core/dispositivos.js.map +1 -0
  271. package/dist/core/entrega.js +141 -0
  272. package/dist/core/entrega.js.map +1 -0
  273. package/dist/core/entrelazar.js +90 -0
  274. package/dist/core/entrelazar.js.map +1 -0
  275. package/dist/core/esqueleto.js +10 -0
  276. package/dist/core/esqueleto.js.map +1 -1
  277. package/dist/core/modelos.js +235 -3
  278. package/dist/core/modelos.js.map +1 -1
  279. package/dist/core/notify.js +27 -9
  280. package/dist/core/notify.js.map +1 -1
  281. package/dist/core/planDeSubida.js +127 -0
  282. package/dist/core/planDeSubida.js.map +1 -0
  283. package/dist/core/ports.js +189 -1
  284. package/dist/core/ports.js.map +1 -1
  285. package/dist/core/puedeLanzarse.js +239 -0
  286. package/dist/core/puedeLanzarse.js.map +1 -0
  287. package/dist/core/rutaVirtual.js +42 -0
  288. package/dist/core/rutaVirtual.js.map +1 -0
  289. package/dist/core/settings.js +246 -0
  290. package/dist/core/settings.js.map +1 -0
  291. package/dist/core/tareas.js +230 -0
  292. package/dist/core/tareas.js.map +1 -0
  293. package/dist/core/textos.js +36 -0
  294. package/dist/core/textos.js.map +1 -0
  295. package/dist/core/turno.js +56 -8
  296. package/dist/core/turno.js.map +1 -1
  297. package/dist/core/version.js +31 -0
  298. package/dist/core/version.js.map +1 -0
  299. package/dist/vendor/sqliteSaver.js +389 -0
  300. package/dist/vendor/sqliteSaver.js.map +1 -0
  301. package/dist/vendor/tokenTracking.js +22 -1
  302. package/dist/vendor/tokenTracking.js.map +1 -1
  303. package/dist/web/servidor/arranque.js +3964 -0
  304. package/dist/web/servidor/arranque.js.map +1 -0
  305. package/dist/web/servidor/consolaDeTarea.js +161 -0
  306. package/dist/web/servidor/consolaDeTarea.js.map +1 -0
  307. package/dist/web/servidor/consolaWeb.js +389 -0
  308. package/dist/web/servidor/consolaWeb.js.map +1 -0
  309. package/dist/web/servidor/corredorDeTareas.js +1119 -0
  310. package/dist/web/servidor/corredorDeTareas.js.map +1 -0
  311. package/dist/web/servidor/pielWeb.js +269 -0
  312. package/dist/web/servidor/pielWeb.js.map +1 -0
  313. package/dist/web/servidor/servidor.js +257 -0
  314. package/dist/web/servidor/servidor.js.map +1 -0
  315. package/dist/web/servidor/sesiones.js +440 -0
  316. package/dist/web/servidor/sesiones.js.map +1 -0
  317. package/dist/web/servidor/transporte.js +169 -0
  318. package/dist/web/servidor/transporte.js.map +1 -0
  319. package/dist/web/servidor/vestibulo.js +1276 -0
  320. package/dist/web/servidor/vestibulo.js.map +1 -0
  321. package/package.json +33 -9
  322. package/skills/archify/LICENSE +22 -0
  323. package/skills/archify/SKILL.md +109 -0
  324. package/skills/archify/THIRD_PARTY_NOTICES.md +56 -0
  325. package/skills/archify/assets/template.html +14787 -0
  326. package/skills/archify/bin/archify.mjs +1990 -0
  327. package/skills/archify/bin/open-artifact.mjs +86 -0
  328. package/skills/archify/bin/preview.mjs +648 -0
  329. package/skills/archify/bin/visual-check.mjs +829 -0
  330. package/skills/archify/brand-marks/README.md +31 -0
  331. package/skills/archify/brand-marks/catalog.json +131 -0
  332. package/skills/archify/delta/architecture-delta.mjs +1214 -0
  333. package/skills/archify/examples/agent-run.lifecycle.json +71 -0
  334. package/skills/archify/examples/agent-tool-call.workflow.json +94 -0
  335. package/skills/archify/examples/async-job-roundtrip.sequence.json +61 -0
  336. package/skills/archify/examples/brand-aware-delivery.architecture.json +47 -0
  337. package/skills/archify/examples/cache-miss-request.sequence.json +81 -0
  338. package/skills/archify/examples/checkout-platform.base.architecture.json +31 -0
  339. package/skills/archify/examples/checkout-platform.head.architecture.json +31 -0
  340. package/skills/archify/examples/dataflow-product-analytics.html +14898 -0
  341. package/skills/archify/examples/deployment-release.lifecycle.json +49 -0
  342. package/skills/archify/examples/event-stream.dataflow.json +57 -0
  343. package/skills/archify/examples/incident-response.workflow.json +64 -0
  344. package/skills/archify/examples/lifecycle-agent-run.html +14847 -0
  345. package/skills/archify/examples/product-analytics.dataflow.json +76 -0
  346. package/skills/archify/examples/production-deployment.architecture.json +71 -0
  347. package/skills/archify/examples/release-delivery.workflow.json +62 -0
  348. package/skills/archify/examples/sequence-cache-miss-request.html +14913 -0
  349. package/skills/archify/examples/web-app-rendered.html +14862 -0
  350. package/skills/archify/examples/web-app.architecture.json +46 -0
  351. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +14904 -0
  352. package/skills/archify/migrations/workflow-v2.mjs +279 -0
  353. package/skills/archify/package.json +17 -0
  354. package/skills/archify/recipes/scenarios.mjs +391 -0
  355. package/skills/archify/references/authoring-contract.md +198 -0
  356. package/skills/archify/references/brand-marks.md +69 -0
  357. package/skills/archify/references/delivery-contract.md +116 -0
  358. package/skills/archify/references/viewer-runtime.md +45 -0
  359. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  360. package/skills/archify/renderers/architecture/render-architecture.mjs +1078 -0
  361. package/skills/archify/renderers/dataflow/README.md +104 -0
  362. package/skills/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  363. package/skills/archify/renderers/lifecycle/README.md +115 -0
  364. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  365. package/skills/archify/renderers/sequence/README.md +114 -0
  366. package/skills/archify/renderers/sequence/render-sequence.mjs +453 -0
  367. package/skills/archify/renderers/shared/brand-marks.mjs +563 -0
  368. package/skills/archify/renderers/shared/cli.mjs +218 -0
  369. package/skills/archify/renderers/shared/desktop-readability.mjs +26 -0
  370. package/skills/archify/renderers/shared/diagnostics.mjs +127 -0
  371. package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
  372. package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  373. package/skills/archify/renderers/shared/generated-validators.mjs +13 -0
  374. package/skills/archify/renderers/shared/geometry.mjs +1423 -0
  375. package/skills/archify/renderers/shared/i18n.mjs +594 -0
  376. package/skills/archify/renderers/shared/layout-report.mjs +40 -0
  377. package/skills/archify/renderers/shared/legend.mjs +217 -0
  378. package/skills/archify/renderers/shared/output-path.mjs +321 -0
  379. package/skills/archify/renderers/shared/repository-evidence.mjs +235 -0
  380. package/skills/archify/renderers/shared/text-fit.mjs +49 -0
  381. package/skills/archify/renderers/shared/utils.mjs +232 -0
  382. package/skills/archify/renderers/shared/validator.mjs +86 -0
  383. package/skills/archify/renderers/workflow/README.md +223 -0
  384. package/skills/archify/renderers/workflow/render-workflow.mjs +35 -0
  385. package/skills/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  386. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  387. package/skills/archify/schemas/README.md +211 -0
  388. package/skills/archify/schemas/architecture.schema.json +176 -0
  389. package/skills/archify/schemas/common.schema.json +115 -0
  390. package/skills/archify/schemas/dataflow.schema.json +243 -0
  391. package/skills/archify/schemas/lifecycle.schema.json +266 -0
  392. package/skills/archify/schemas/sequence.schema.json +223 -0
  393. package/skills/archify/schemas/workflow.schema.json +428 -0
  394. package/skills/archify/scripts/check-render-output.mjs +835 -0
  395. package/skills/archify/scripts/check-update.mjs +1667 -0
  396. package/skills/archify/scripts/render-examples.mjs +26 -0
  397. package/skills/archify/scripts/update-contract.mjs +182 -0
  398. package/skills/archify/skill-release.json +10 -0
  399. package/skills/artifacts-builder/SKILL.md +120 -0
  400. package/skills/artifacts-builder/reference/diagramas.md +123 -0
  401. package/skills/artifacts-builder/reference/estilo.md +123 -0
  402. package/skills/artifacts-builder/reference/exportar-pdf.md +56 -0
  403. package/skills/artifacts-builder/reference/graficas.md +22 -0
  404. package/skills/artifacts-builder/scripts/bundle_artifact.py +109 -0
  405. package/skills/artifacts-builder/scripts/validate_artifact.py +345 -0
  406. package/skills/xone-hotswap/SKILL.md +185 -0
  407. package/skills/xone-hotswap/references/comandos.md +706 -0
  408. package/skills/xone-hotswap/references/conexion-y-despliegue.md +226 -0
@@ -0,0 +1,1276 @@
1
+ /**
2
+ * El vestíbulo: lo que hay ANTES de que exista ninguna raíz.
3
+ *
4
+ * `correrConsola` es un lazo sobre UNA `raiz`, y la jerarquía entorno → proyecto → sesión
5
+ * necesita un sitio donde vivir mientras no hay proyecto abierto. De ahí las dos clases de
6
+ * consola: el vestíbulo (sin raíz) y la consola de proyecto (con la suya).
7
+ *
8
+ * Los pasos son los mismos del alta de terminal, y cada uno solo aparece si falta lo que
9
+ * decide. El de cuenta se detecta como siempre: si el papel `trabajo` resuelve por
10
+ * `omision`, nadie eligió nunca. **No hay marca de «primer arranque»**: sería una segunda
11
+ * fuente de verdad sobre algo que el sistema ya sabe.
12
+ *
13
+ * Este fichero NO toca disco ni red por su cuenta. Todo lo que escribe o conecta entra por
14
+ * la opción correspondiente, y las que escriben (`guardarCredencial`, `guardarEntorno`,
15
+ * `guardarConfigDeProyecto`, `descargar`) son OBLIGATORIAS: un valor por omisión que
16
+ * escribiera en el `~/.xonecode` de verdad convertiría cualquier test en una escritura en
17
+ * la casa del usuario, y uno que no escribiera sería el no-op silencioso que este repo
18
+ * evita en todas partes.
19
+ *
20
+ * La única LECTURA de disco con valor por omisión es `esProyecto` («¿hay copia local
21
+ * aquí?»), y sigue la misma regla que `sesiones ?? SESIONES_EN_DISCO`: entra por opción
22
+ * para que un test pueda contestarla sin carpetas, y su omisión es la de verdad porque
23
+ * mentir en esa pregunta abre un proyecto que no existe.
24
+ */
25
+ import { randomUUID } from "node:crypto";
26
+ import { existsSync } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { join } from "node:path";
29
+ import { consumoDeLosActos, sumarConsumo } from "../../core/actos.js";
30
+ import { consumoDeLaSesion, consumoPersistible, esDoble } from "../../core/ports.js";
31
+ import { rutaDeWorkspace } from "../../core/settings.js";
32
+ import { URL_CLOUDSTUDIO_POR_OMISION, SCOPES_CLOUDSTUDIO_AGENTE, AVISO_DE_URL_DE_MCP, adoptarLegadoSiProcede, conectarCloudStudio, rutaAuthPorDefecto, sesionCloudStudio, urlDeMcpAceptable, } from "../../agent/cloudstudioMcp.js";
33
+ import { clienteCloudStudio } from "../../agent/cloudstudioClient.js";
34
+ import { guardarCloudStudioDeProyecto, guardarEntornoDeProyecto, guardarModeloGlobal as guardarModeloGlobalEnDisco, guardarModoDeProyecto, guardarProyectoCloudStudioDeProyecto, guardarRamaDeProyecto, } from "../../agent/configEnDisco.js";
35
+ import { correrConsola, ejecutarTurnoGuionizado, } from "../../cli/consola.js";
36
+ import { asistenteDeModelo } from "../../cli/wizardInicial.js";
37
+ import { crearConsolaWeb } from "./consolaWeb.js";
38
+ import { anotarActo, borrarSesion, crearSesion, elegirDispositivo, listarSesiones, reabrirSesion, renombrarSesion, } from "./sesiones.js";
39
+ /**
40
+ * Los dos CloudStudio oficiales, pre-rellenados.
41
+ *
42
+ * La URL de WebStudio no se repite aquí: se importa de `agent/cloudstudioMcp.ts`, que es
43
+ * donde vive desde que `adoptarLegadoSiProcede` la necesitó (`cli/consola.ts` la reexporta
44
+ * por compatibilidad). Dos literales de la misma dirección es cómo divergen el día que una
45
+ * se corrige y la otra no — el mismo motivo por el que `segmentoSeguro` no se copió.
46
+ */
47
+ export const ENTORNOS_OFICIALES = [
48
+ { id: "webstudio", nombre: "XOne WebStudio", url: URL_CLOUDSTUDIO_POR_OMISION },
49
+ { id: "manager", nombre: "XOne Manager", url: "https://mcp.xonemanager.com/mcp" },
50
+ ];
51
+ /**
52
+ * El tercero de la lista, y no un botón aparte de la interfaz: un on-premise es un entorno
53
+ * como los otros dos, y presentarlo al mismo nivel es lo que evita que la web tenga dos
54
+ * caminos distintos para registrar lo mismo.
55
+ */
56
+ export const ENTORNO_OTRO = { id: "otro", nombre: "Otro (on-premise)", url: "" };
57
+ /** La base del workspace por omisión: `~/.xonecode`. La disposición de dentro la fija
58
+ * `rutaDeWorkspace` y no es configurable. */
59
+ export function baseDeWorkspacePorOmision() {
60
+ return join(homedir(), ".xonecode");
61
+ }
62
+ const SESIONES_EN_DISCO = {
63
+ crear: crearSesion,
64
+ listar: listarSesiones,
65
+ anotar: anotarActo,
66
+ reabrir: reabrirSesion,
67
+ borrar: borrarSesion,
68
+ renombrar: renombrarSesion,
69
+ elegirDispositivo,
70
+ };
71
+ /**
72
+ * ¿Hay una copia local de proyecto en esa raíz? Es el MISMO criterio con el que el alta
73
+ * decide `proyectos[].local` (`arranque.ts#hayCopiaLocal`, que tira de aquí): existe su
74
+ * `.xonecode/config.json`. Dos copias del predicado es cómo divergen el día que una se
75
+ * afine — el mismo motivo por el que la regla de URL de MCP vive en un solo sitio.
76
+ *
77
+ * Se lo traga todo y devuelve `false`: una raíz que no se puede ni mirar (permisos, un
78
+ * enlace roto) no es un proyecto sobre el que abrir nada, y esta puerta falla CERRADO.
79
+ */
80
+ export function esProyectoEnDisco(raiz) {
81
+ // Sin `try`: `existsSync` no lanza nunca —traga cualquier error del sistema y devuelve
82
+ // false—, así que envolverlo sugeriría un peligro que no existe.
83
+ return existsSync(join(raiz, ".xonecode", "config.json"));
84
+ }
85
+ /**
86
+ * Lo que se contesta a quien intenta abrir —o borrar— la conversación de una tarea que sigue
87
+ * corriendo. Vive en una constante porque lo leen las dos guardas y su test, y porque es lo
88
+ * ÚNICO que el usuario ve de esta regla.
89
+ *
90
+ * **Dice qué SÍ se puede hacer**, y no es cortesía: la vista en vivo existe justo al lado, así
91
+ * que un «no puedes» a secas es lo que hace que alguien insista o dé el botón por roto. Y no
92
+ * lleva ninguna ruta de la máquina: sale por el cable, que puede ir por un túnel.
93
+ */
94
+ export function motivoDeProyectoTrabajando() {
95
+ return ("ese proyecto tiene una sesión con un turno en marcha: dos conversaciones sobre la misma " +
96
+ "copia de trabajo se pisarían los ficheros. Vuelve a ella para verla, o párala, y abre la " +
97
+ "otra cuando termine.");
98
+ }
99
+ export const MOTIVO_SESION_DE_TAREA_EN_CURSO = "esa conversación es la de una tarea en curso: el agente está escribiendo en ella ahora " +
100
+ "mismo. Pulsa «Ver lo que hace» en su tarjeta para mirarla, y ábrela cuando termine.";
101
+ /**
102
+ * El `config.json` del proyecto, escrito con los escritores que ya existen.
103
+ *
104
+ * El orden importa y `modo` va el ÚLTIMO a propósito: si algo revienta a mitad, lo que
105
+ * queda en disco es un `cloudstudio` sin `modo`, que es exactamente el caso que
106
+ * `crearSincronizador` ya sabe contar («este proyecto no es cloud…»). Al revés dejaría un
107
+ * proyecto declarado cloud sin saber contra qué sincronizar.
108
+ *
109
+ * `entorno` se escribe ADEMÁS de `cloudstudio.url`, nunca en su lugar: conservar la URL es
110
+ * lo que hace literalmente cierto que la sincronización no se toca — `crearSincronizador`
111
+ * y todo lo que cuelga de él la leen igual que hoy.
112
+ */
113
+ export function escribirProyectoEnDisco(raiz, datos) {
114
+ guardarCloudStudioDeProyecto(raiz, datos.url, datos.scopes);
115
+ guardarProyectoCloudStudioDeProyecto(raiz, datos.proyecto);
116
+ guardarRamaDeProyecto(raiz, datos.rama);
117
+ guardarEntornoDeProyecto(raiz, datos.entorno);
118
+ return { ruta: guardarModoDeProyecto(raiz, "cloud").ruta };
119
+ }
120
+ /**
121
+ * Los dos cables de CloudStudio del vestíbulo, con el id del entorno y la vuelta a la web
122
+ * ya enhebrados.
123
+ *
124
+ * Las dos cosas son deudas que se pagan aquí: sin `entornoId`, un proyecto ya migrado a
125
+ * `webstudio` reautenticaría en un segundo hueco `legado`; y sin `redirigirA`, la página
126
+ * del callback termina diciendo «vuelve a la terminal», que en un navegador es falso.
127
+ */
128
+ export function conexionDeVestibulo(urlDeLaWeb) {
129
+ const comunes = (entorno) => ({
130
+ scopes: entorno.scopes ?? SCOPES_CLOUDSTUDIO_AGENTE,
131
+ entornoId: entorno.id,
132
+ ...(urlDeLaWeb === undefined ? {} : { redirigirA: urlDeLaWeb }),
133
+ });
134
+ return {
135
+ proyectosDeEntorno: async (entorno) => {
136
+ const conexion = await conectarCloudStudio(entorno.url, comunes(entorno));
137
+ return {
138
+ proyectos: conexion.proyectos,
139
+ ...(conexion.servidor === undefined ? {} : { servidor: { nombre: conexion.servidor.nombre } }),
140
+ };
141
+ },
142
+ ramasDeProyecto: async (entorno, proyecto) => {
143
+ const sesion = await sesionCloudStudio(entorno.url, comunes(entorno));
144
+ try {
145
+ const puerto = clienteCloudStudio(sesion.invocar, proyecto);
146
+ await puerto.abrir(proyecto);
147
+ return await puerto.ramas();
148
+ }
149
+ finally {
150
+ await sesion.cerrar();
151
+ }
152
+ },
153
+ };
154
+ }
155
+ /**
156
+ * La regla de URL de entorno es la MISMA que la de quien conecta de verdad
157
+ * (`agent/cloudstudioMcp.ts#urlDeMcpAceptable`), importada y no copiada: cuando eran dos
158
+ * criterios, el wizard aceptaba un loopback que este fichero rechazaba, con dos mensajes
159
+ * claros que se contradecían. Ver allí por qué se resolvió por el lado permisivo.
160
+ */
161
+ const urlDeEntornoValida = urlDeMcpAceptable;
162
+ /**
163
+ * El host de una URL, o la URL entera si no parsea. No debería hacer falta ese segundo caso
164
+ * —`urlDeEntornoValida` corre antes y ya rechaza lo que no parsea—, pero si algún día
165
+ * alguien invierte ese orden, mejor un id feo que una excepción.
166
+ */
167
+ function hostDeUrl(url) {
168
+ try {
169
+ return new URL(url).host;
170
+ }
171
+ catch {
172
+ return url;
173
+ }
174
+ }
175
+ /** Misma URL, dicha de dos formas (`https://x/mcp` y `https://x/mcp/`), es el mismo sitio. */
176
+ function mismaUrl(a, b) {
177
+ const normal = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
178
+ return normal(a) === normal(b);
179
+ }
180
+ /**
181
+ * La identidad del entorno, DEDUCIDA de la URL cuando no la trae puesta.
182
+ *
183
+ * El formulario del navegador pide solo la URL: un nombre tecleado a mano es un dato que
184
+ * el usuario se inventa y que después hay que creerse en la barra lateral y en la ruta del
185
+ * workspace. Las dos reglas:
186
+ *
187
+ * - Si la URL es la de un entorno OFICIAL, se usa su identidad (aunque se haya escrito a
188
+ * mano en el hueco de «otro»): dos entradas para el mismo servidor son dos carpetas de
189
+ * workspace y dos huecos de OAuth para la misma cuenta.
190
+ * - Si no, el id y el nombre salen del HOST. El id pasa por `segmentoSeguro`
191
+ * (`core/settings.ts`) porque acaba siendo una carpeta (`rutaDeWorkspace`), así que se
192
+ * quita todo lo que no sea letra, cifra, punto o guion — los dos puntos del puerto
193
+ * incluidos, que en Windows parten la ruta. El nombre conserva el host tal cual, que es
194
+ * para leerlo.
195
+ *
196
+ * Lo que NO se deduce aquí es nada que venga del servidor: eso exige haber hecho OAuth, y
197
+ * el registro pasa antes. Un nombre inventado a partir de la URL es verdad comprobable;
198
+ * uno «bonito» sacado de la nada no lo sería.
199
+ */
200
+ export function identidadDeEntorno(entorno) {
201
+ const oficial = ENTORNOS_OFICIALES.find((o) => mismaUrl(o.url, entorno.url));
202
+ if (oficial !== undefined)
203
+ return { ...entorno, id: oficial.id, nombre: oficial.nombre };
204
+ // Un id y un nombre puestos por quien llama se respetan: el paso de entorno del wizard
205
+ // manda «otro» (o vacío) justo para pedir esta deducción, pero otra piel puede traerlos.
206
+ if (entorno.id !== "" && entorno.id !== ENTORNO_OTRO.id && entorno.nombre !== "")
207
+ return entorno;
208
+ const host = hostDeUrl(entorno.url);
209
+ const id = host.replace(/[^a-zA-Z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
210
+ return {
211
+ ...entorno,
212
+ id: id === "" ? "on-premise" : id,
213
+ nombre: entorno.nombre === "" ? host : entorno.nombre,
214
+ };
215
+ }
216
+ export function crearVestibulo(opciones) {
217
+ const sesiones = opciones.sesiones ?? SESIONES_EN_DISCO;
218
+ const crearConsola = opciones.crearConsola ?? crearConsolaWeb;
219
+ const correr = opciones.correr ?? correrConsola;
220
+ const base = opciones.baseDeWorkspace ?? baseDeWorkspacePorOmision();
221
+ const adoptarLegado = opciones.adoptarLegado ??
222
+ // `rutaAuthPorDefecto` y no un `join` propio: el mismo literal en dos ficheros es lo
223
+ // que esta cabecera condena, y `cloudstudioMcp.ts` la exporta justo para esto.
224
+ ((entorno) => adoptarLegadoSiProcede(rutaAuthPorDefecto(), entorno));
225
+ const guardarModeloGlobal = opciones.guardarModeloGlobal ?? guardarModeloGlobalEnDisco;
226
+ const esProyecto = opciones.esProyecto ?? esProyectoEnDisco;
227
+ const consolaDelVestibulo = crearConsola({
228
+ catalogoModelos: opciones.catalogoModelos,
229
+ guardarModeloGlobal,
230
+ ...(opciones.msDeEspera === undefined ? {} : { msDeEspera: opciones.msDeEspera }),
231
+ });
232
+ const informar = opciones.informar ?? ((texto) => consolaDelVestibulo.consola.escribir(`${texto}\n`));
233
+ /** Los registrados al arrancar MÁS los que este vestíbulo acaba de dar de alta: el paso 3
234
+ * llega justo después del 2 y `settings.json` no se vuelve a leer entre medias. */
235
+ const registrados = [...(opciones.entornos ?? [])];
236
+ /** Lo ya escrito, para que `cancelar()` pueda decirlo en vez de fingir que no pasó nada. */
237
+ let credencialEscrita;
238
+ let proyectoEscrito;
239
+ /**
240
+ * Las consolas de PERSONA vivas, por raíz. Antes era una sola variable, y cambiar de
241
+ * sesión mataba el turno que estuviera corriendo.
242
+ *
243
+ * **La clave es la raíz, y eso es la mitad del diseño**: nunca hay dos consolas sobre la
244
+ * misma copia de trabajo, así que dos conversaciones no pueden escribir el mismo fichero
245
+ * ni pisarse el hilo del checkpointer. Lo que se gana es lo otro: dos proyectos distintos
246
+ * pueden estar trabajando a la vez, y mirar uno no interrumpe al otro.
247
+ *
248
+ * No crece sin freno: una consola de segundo plano se cierra ella sola en cuanto su turno
249
+ * acaba (`cerrarSiSobra`). El hilo no se pierde —el checkpointer lo reanuda al reabrir la
250
+ * sesión—, así que cerrarla no cuesta nada y dejarla viva costaría un `correrConsola` por
251
+ * proyecto visitado.
252
+ */
253
+ const abiertas = new Map();
254
+ /** La raíz de la consola que el cable está pintando. `undefined` = el vestíbulo. */
255
+ let enFoco;
256
+ const consolaEnFoco = () => enFoco === undefined ? undefined : abiertas.get(enFoco);
257
+ /** Quien quiera enterarse de que el modelo en vigor cambió. UNA sola: el cable es uno. */
258
+ let escuchaDeEstado;
259
+ /** Y de que hay (o deja de haber) un turno corriendo. */
260
+ let escuchaDeTurno;
261
+ /** Y de que la sesión en foco ha consumido más tokens. */
262
+ let escuchaDeConsumo;
263
+ /**
264
+ * Avisa de que algo cambió en las consolas de persona: un turno empieza o acaba —en
265
+ * CUALQUIERA de ellas— o una de segundo plano se cierra.
266
+ *
267
+ * El booleano sigue siendo el de siempre: «¿hay turno en vuelo en la que se está
268
+ * mirando?». Lo que se ensanchó es el DISPARADOR, y hacía falta: sin eso, un turno de
269
+ * segundo plano que arranca o termina no repintaría la barra, que es donde se ve qué
270
+ * sesión está trabajando.
271
+ *
272
+ * `deLaDeAhora` existe por una trampa de orden: el primer flanco de un turno puede
273
+ * ocurrir ANTES de que la consola esté en el mapa —`construirConsolaDeProyecto` arranca
274
+ * su lazo dentro, y un `correr` que ejecute un turno en el acto (los tests, y cualquier
275
+ * ejecutor que no espere una línea) lo dispara desde ahí—. Preguntándole al mapa se
276
+ * habría contestado «no hay turno» justo en el flanco que lo anuncia. Quien llama sí sabe
277
+ * de qué consola habla, así que lo dice.
278
+ */
279
+ const avisarDeLasAbiertas = (deLaDeAhora) => {
280
+ const enfocada = deLaDeAhora !== undefined && deLaDeAhora.raiz === enFoco
281
+ ? deLaDeAhora.turnoEnVuelo
282
+ : consolaEnFoco()?.turnoEnVuelo === true;
283
+ escuchaDeTurno?.(enfocada);
284
+ };
285
+ const entornoPorId = (id) => {
286
+ const encontrado = registrados.find((e) => e.id === id);
287
+ if (encontrado === undefined) {
288
+ throw new Error(`el entorno «${id}» no está registrado en settings.json`);
289
+ }
290
+ return encontrado;
291
+ };
292
+ /** Escribe la credencial y la RECUERDA, sin decir nada: quien llama decide si lo dice,
293
+ * porque `asistenteDeModelo` ya lo dice por su cuenta y decirlo dos veces es ruido. */
294
+ const registrarCredencial = (proveedor, clave) => {
295
+ const guardada = opciones.guardarCredencial(proveedor, clave);
296
+ credencialEscrita = { proveedor, ruta: guardada.ruta };
297
+ return guardada;
298
+ };
299
+ /**
300
+ * El nombre BUENO del entorno, el que dice el propio servidor, cuando por fin se ha
301
+ * hablado con él.
302
+ *
303
+ * Del alta solo sale la URL, así que hasta aquí el nombre era el host — verdad
304
+ * comprobable, pero fea («mcp.casa.local»). El `serverInfo` del initialize llega en la
305
+ * primera conexión de verdad, que es `proyectosDe`, y solo entonces se puede sustituir.
306
+ *
307
+ * Dos guardas, y las dos importan:
308
+ * - Solo se pisa un nombre DEDUCIDO (el que sigue siendo igual al host). Un nombre que
309
+ * puso una persona —o el de un entorno oficial, «XOne WebStudio»— no lo cambia un
310
+ * servidor remoto por su cuenta.
311
+ * - El id NO se toca nunca. Es un segmento de ruta (`rutaDeWorkspace`) y ya hay una copia
312
+ * local colgando de él: cambiarlo aquí sería mudar la carpeta del proyecto de sitio
313
+ * porque el servidor decidió llamarse de otra forma.
314
+ */
315
+ const renombrarConElServidor = (entorno, nombreDelServidor) => {
316
+ if (nombreDelServidor === undefined || nombreDelServidor === "")
317
+ return;
318
+ if (entorno.nombre !== hostDeUrl(entorno.url))
319
+ return;
320
+ if (nombreDelServidor === entorno.nombre)
321
+ return;
322
+ const renombrado = { ...entorno, nombre: nombreDelServidor };
323
+ opciones.guardarEntorno(renombrado);
324
+ const donde = registrados.findIndex((e) => e.id === entorno.id);
325
+ if (donde >= 0)
326
+ registrados.splice(donde, 1, renombrado);
327
+ informar(`entorno «${entorno.id}»: el servidor dice llamarse «${nombreDelServidor}»`);
328
+ };
329
+ /**
330
+ * Cierra la consola de ESA raíz, si hay alguna.
331
+ *
332
+ * Se ESPERA a que el lazo retorne. Dos `correrConsola` vivos sobre la misma copia de
333
+ * trabajo comparten el ejecutor real y se pisarían el hilo — que es exactamente lo que la
334
+ * clave por raíz del mapa impide, y esto es la otra mitad: se saca del mapa DESPUÉS de
335
+ * que su lazo haya terminado, así que entre medias nadie la puede reclamar como libre.
336
+ */
337
+ const cerrarConsolaDeProyecto = async (raiz) => {
338
+ const consola = abiertas.get(raiz);
339
+ if (consola === undefined)
340
+ return;
341
+ await consola.cerrar();
342
+ abiertas.delete(raiz);
343
+ if (enFoco === raiz)
344
+ enFoco = undefined;
345
+ };
346
+ /** Todas: lo que hacen `cancelar()` y `cerrar()`. Un fallo al cerrar una no puede dejar a
347
+ * las demás vivas — es la misma regla que `cerrarLasDeTareas`. */
348
+ const cerrarLasDeProyecto = async () => {
349
+ for (const raiz of [...abiertas.keys()]) {
350
+ await cerrarConsolaDeProyecto(raiz).catch(() => abiertas.delete(raiz));
351
+ }
352
+ enFoco = undefined;
353
+ };
354
+ /**
355
+ * Cierra las consolas de segundo plano que NO están trabajando, menos la de esa raíz.
356
+ *
357
+ * Es la otra mitad del acotado, y va ANTES de construir la nueva: lo que se queda vivo en
358
+ * segundo plano es solo lo que tiene un turno en marcha. Sin esto, visitar diez proyectos
359
+ * dejaría diez `correrConsola` corriendo para siempre — y de paso se conserva el orden
360
+ * que ya estaba probado: el lazo anterior TERMINA antes de que arranque el siguiente, que
361
+ * es lo normal porque lo normal es cambiar de sesión sin nada en vuelo.
362
+ */
363
+ const cerrarLasOciosasSalvo = async (raizQueSigue) => {
364
+ for (const [raiz, consola] of [...abiertas]) {
365
+ if (raiz === raizQueSigue)
366
+ continue;
367
+ if (consola.turnoEnVuelo && !consola.cerrada)
368
+ continue;
369
+ await cerrarConsolaDeProyecto(raiz).catch(() => abiertas.delete(raiz));
370
+ }
371
+ };
372
+ /**
373
+ * Una consola de segundo plano que acaba de terminar su turno ya no hace falta: se cierra.
374
+ *
375
+ * Es lo que hace que «varias consolas vivas» sea acotado en vez de una fuga: sin esto,
376
+ * cada proyecto que alguien visitara dejaría un `correrConsola` corriendo para siempre.
377
+ * Y no se pierde nada al cerrarla — el hilo del agente lo reanuda el checkpointer cuando
378
+ * esa sesión se vuelva a abrir; lo único que se paga es el arranque.
379
+ *
380
+ * Va POR LA COLA y no en el acto: quien llama es el `finally` del propio turno, y
381
+ * `cerrar()` aguarda `terminada`, o sea el lazo que todavía está dentro de ese turno.
382
+ * Encolarlo lo deja para cuando la pasada haya devuelto. Las tres condiciones se vuelven a
383
+ * mirar ahí dentro, porque entre medias el foco puede haber vuelto a este proyecto o
384
+ * puede haber empezado otro turno.
385
+ */
386
+ const cerrarSiSobra = (raiz) => {
387
+ void enCola(async () => {
388
+ if (enFoco === raiz)
389
+ return;
390
+ const consola = abiertas.get(raiz);
391
+ if (consola === undefined || consola.turnoEnVuelo)
392
+ return;
393
+ if (!consola.cerrada)
394
+ await cerrarConsolaDeProyecto(raiz);
395
+ else
396
+ abiertas.delete(raiz);
397
+ // Se vuelve a avisar: esta raíz acaba de quedar LIBRE, y de eso cuelgan la barra y la
398
+ // cola de tareas («gana la persona» deja de aplicar aquí). Avisar solo en el flanco
399
+ // del turno habría contado la raíz como ocupada, porque este cierre viene después.
400
+ avisarDeLasAbiertas();
401
+ }).catch(() => undefined);
402
+ };
403
+ /**
404
+ * La cola de las operaciones que tocan `abiertas`.
405
+ *
406
+ * «Una consola por raíz» no se sostiene sin serializar: dos `POST` que lleguen a la vez se
407
+ * entrelazan en el `await` del cierre, los dos ven la raíz libre, los dos arrancan un
408
+ * `correrConsola` y el primero se queda vivo y sin nadie que lo pueda cerrar nunca. La
409
+ * cola se traga los rechazos —si no, un fallo
410
+ * dejaría la cadena rota para todo lo que venga detrás— y el rechazo de verdad se
411
+ * devuelve sin tragar a quien llamó.
412
+ */
413
+ let cola = Promise.resolve();
414
+ const enCola = (operacion) => {
415
+ const resultado = cola.then(operacion);
416
+ cola = resultado.catch(() => undefined);
417
+ return resultado;
418
+ };
419
+ /**
420
+ * Construye una consola de proyecto — y NADA de la contabilidad del cable.
421
+ *
422
+ * Se extrajo de `abrirDeVerdad` cuando apareció la segunda puerta (`abrirParaTarea`), y
423
+ * la línea del corte es exactamente esa: aquí dentro está TODO lo que decide con qué
424
+ * barreras corre el agente —el ejecutor real por `crearEjecutor`, de donde cuelga el
425
+ * backend con las vistas aplanadas retiradas y la guarda de artefactos; las dependencias
426
+ * de la raíz; la foto del antes; el volcado— y fuera se queda solo quién está enganchado
427
+ * al cable (`abierto`, y el cierre de lo anterior).
428
+ *
429
+ * Que sea UNA función no es comodidad: si las dos puertas divergieran, una tarea correría
430
+ * con menos barreras que una persona, y eso no se ve — se descubre cuando el agente ya
431
+ * escribió. Lo único que `alCable` apaga son los dos AVISOS al navegador; ni una regla.
432
+ */
433
+ const construirConsolaDeProyecto = async ({ raiz, sesion, adjuntos, tarea, enElCable, alFlancoDeTurno, }) => {
434
+ const reabierta = sesion === undefined ? undefined : sesiones.reabrir(raiz, sesion);
435
+ /**
436
+ * Lo que costó esta conversación ANTES de este proceso, leído de los `fin` que el
437
+ * `.jsonl` ya tenía.
438
+ *
439
+ * **Se lee UNA vez, aquí, y no se vuelve a mirar.** El fichero CRECE por debajo mientras
440
+ * este proceso vive —`volcar()` anota cada turno—, así que releerlo a mitad de sesión
441
+ * contaría dos veces los turnos que ya están en el tracker vivo. Esa es la regla que se
442
+ * puede romper en silencio dentro de un año, y por eso la base solo se fija en este
443
+ * punto: a partir de aquí, lo que crezca lo aporta la sesión viva.
444
+ *
445
+ * `undefined` cuando ningún `fin` lo trae, que es una sesión escrita antes de que esto
446
+ * existiera. No se rellena con ceros: un cero que nadie ha medido es la cifra inventada
447
+ * de siempre, y aquí se distinguen «anterior» y «gastó cero».
448
+ */
449
+ const consumoHistorico = reabierta === undefined ? undefined : consumoDeLosActos(reabierta.actos);
450
+ /**
451
+ * La sesión real, que nace DENTRO del primer turno —`crearEjecutorReal` avisa después de
452
+ * `inspeccionar` y de `abrirSesionReal`—. Se declara aquí arriba, antes de construir la
453
+ * consola, porque quien pregunta por el consumo se construye ANTES que ella.
454
+ */
455
+ let sesionReal;
456
+ /**
457
+ * Lo consumido por la sesión VIVA, sin la base histórica.
458
+ *
459
+ * **Es la fuente de la piel, y no la de la pantalla.** Son dos lecturas distintas a
460
+ * propósito: la piel resta este valor contra el que estampó en el `fin` anterior para
461
+ * sacar el gasto del turno, y si aquí entrara la base histórica el PRIMER `fin` de una
462
+ * sesión reabierta estamparía la base entera como si se hubiera gastado otra vez — y
463
+ * reabrir tres veces triplicaría el total. Lo que se pinta es la suma, y esa se compone
464
+ * abajo.
465
+ */
466
+ const consumoVivo = () => {
467
+ const vivo = sesionReal?.consumo?.();
468
+ return vivo === undefined ? undefined : consumoPersistible(vivo);
469
+ };
470
+ const consolaWeb = crearConsola({
471
+ catalogoModelos: opciones.catalogoModelos,
472
+ guardarModeloGlobal,
473
+ ...(opciones.msDeEspera === undefined ? {} : { msDeEspera: opciones.msDeEspera }),
474
+ consumoAcumulado: consumoVivo,
475
+ });
476
+ // `Partial<Consola>` sobre el objeto recién creado: lo que depende de la raíz (`/sync`,
477
+ // los escritores del proyecto) no lo puede saber `consolaWeb`, que no conoce ninguna.
478
+ Object.assign(consolaWeb.consola, opciones.dependenciasDeProyecto?.(raiz) ?? {});
479
+ // La foto del ANTES se toma AQUÍ, al abrir, no en el primer volcado: para entonces el
480
+ // agente ya habría escrito y el «antes» incluiría su trabajo. Se nombra más abajo,
481
+ // cuando el id existe. Se lanza sin esperar —abrir no puede quedarse esperando a git— y
482
+ // el rechazo se traga: sin marca, la vista lo dice.
483
+ const foto = opciones.marcarSesion?.(raiz).catch(() => undefined);
484
+ // La misma regla que la foto de arriba, en el mismo instante y por el mismo motivo:
485
+ // medido después, el «ya había trabajo de alguien» incluiría el de esta sesión. Se
486
+ // lanza sin esperar y el rechazo se traga: es un dato accesorio y abrir un proyecto no
487
+ // puede depender de que git conteste.
488
+ const trabajoAlAbrir = Promise.resolve(opciones.sinCommitear?.(raiz)).catch(() => undefined);
489
+ /**
490
+ * La ref ya APUNTADA, para poder esperarla al cerrar.
491
+ *
492
+ * `volcar()` la lanza y no la aguarda —es síncrono y corre en el `finally` de cada
493
+ * turno—, y eso está bien para quien mira: la pestaña Revisión pregunta cuando alguien
494
+ * la abre, mucho después. Lo que no vale es para quien MIDE: el corredor de tareas
495
+ * comprueba que lo escrito se pueda revisar justo después de cerrar la consola, y
496
+ * medido con git de verdad la ref todavía no estaba —`cambiosDeSesion` devolvía
497
+ * `sin-marca`— así que TODA tarea se habría aparcado diciendo que nadie puede revisarla
498
+ * en un proyecto donde sí se puede. Guardarla aquí y esperarla en `cerrar()` es lo que
499
+ * lo cierra sin que abrir un proyecto tenga que aguardar a git.
500
+ */
501
+ let marcado;
502
+ /**
503
+ * El id de la sesión, decidido al ABRIR y no en el primer volcado.
504
+ *
505
+ * Es el `thread_id` del grafo, y sin esa igualdad no hay nada que reanudar: al reabrir
506
+ * hay que preguntarle al checkpointer por la misma cadena con la que se escribió. Antes
507
+ * nacía en `volcar()` y el hilo era otro uuid, así que reabrir una sesión abría un hilo
508
+ * que no existía en ninguna parte.
509
+ *
510
+ * Lo que sigue siendo PEREZOSO es la entrada del índice (`anotada`): un uuid en memoria
511
+ * no ensucia nada, pero una entrada escrita al abrir dejaría una sesión vacía en la
512
+ * barra cada vez que alguien mira un proyecto y se va sin decir nada. Y un hilo sin
513
+ * turnos tampoco deja checkpoint: LangGraph solo escribe cuando el grafo corre.
514
+ */
515
+ const idSesion = sesion ?? randomUUID();
516
+ /** ¿Tiene ya entrada en el índice? Reabrir implica que sí. */
517
+ let anotada = sesion !== undefined;
518
+ /**
519
+ * Reabrir ya no implica releer: si el checkpointer guarda ese hilo, la conversación
520
+ * CONTINÚA de verdad y el aviso no sale. Solo se pregunta al reabrir —una sesión nueva
521
+ * no puede tener memoria— y una respuesta negativa deja el aviso puesto, que es el lado
522
+ * conservador: enseñarlo de más es preferible a prometer una memoria que no está.
523
+ */
524
+ let historica = reabierta?.historica ?? false;
525
+ if (historica && (await opciones.hayMemoriaDeHilo?.(raiz, sesion)) === true)
526
+ historica = false;
527
+ let dispositivo = reabierta?.dispositivo;
528
+ let cerrada = false;
529
+ /** Ver `ConsolaDeProyecto.turnoEnVuelo`: lo pone y lo quita el envoltorio de abajo, que
530
+ * es el único sitio que ve los dos flancos. */
531
+ let turnoEnVuelo = false;
532
+ let volcados = 0;
533
+ /**
534
+ * Marcado desde que empieza el cierre. Hace falta porque `crearEjecutorReal` avisa de
535
+ * la sesión DESPUÉS de `inspeccionar` y `abrirSesionReal` —segundos en el primer
536
+ * turno—, así que un cierre en esa ventana encontraba `sesionReal` sin definir, no
537
+ * abortaba nada, y el turno seguía corriendo entero sobre un proyecto que el usuario
538
+ * ya había dejado. La sesión que llega tarde se cierra en cuanto se anuncia.
539
+ */
540
+ let cerrando = false;
541
+ const base = opciones.crearEjecutor?.((s) => {
542
+ sesionReal = s;
543
+ if (cerrando)
544
+ s.cerrar();
545
+ /**
546
+ * La cuenta de tokens se escucha AQUÍ y no al construir la consola porque la sesión
547
+ * se anuncia tarde: `crearEjecutorReal` avisa después de `inspeccionar` y
548
+ * `abrirSesionReal`, o sea segundos dentro del primer turno. Suscribirse antes sería
549
+ * suscribirse a algo que no existe.
550
+ *
551
+ * Solo se avisa si esta consola es la que el cable está pintando: los tokens de un
552
+ * turno de segundo plano no pueden cambiar el contador que alguien está mirando en
553
+ * otra sesión — la misma regla que hace que sus actos no se cuelen en ese chat.
554
+ */
555
+ s.alCambiarConsumo?.(() => {
556
+ if (enFoco === raiz)
557
+ escuchaDeConsumo?.();
558
+ });
559
+ },
560
+ // Solo si la hay: `undefined` es lo que reciben las aperturas de persona, y de ahí
561
+ // sale que su agente no tenga `/adjuntos/` montada.
562
+ adjuntos === undefined ? undefined : { adjuntos });
563
+ // El ejecutor que de verdad va a correr los turnos de ESTA consola de proyecto — se
564
+ // fija UNA vez, aquí, para que `volcar` y `ejecutarTurno` miren siempre el mismo valor.
565
+ const ejecutorEfectivo = base ?? ejecutarTurnoGuionizado;
566
+ /**
567
+ * Vuelca al `.jsonl` los actos nuevos.
568
+ *
569
+ * Se llama en las FRONTERAS de turno y al cerrar, nunca a mitad: durante un turno el
570
+ * último acto de la piel todavía muta (el cierre de una racha de tools SUSTITUYE a su
571
+ * apertura dentro del mismo acto, `core/actos.ts#conLineaDeTool`), y un `.jsonl` que
572
+ * solo sabe anexar guardaría las dos líneas. El coste, medido y asumido: un cierre
573
+ * abrupto a mitad de turno pierde ese turno, no la sesión.
574
+ *
575
+ * La sesión se crea PEREZOSAMENTE, en el primer volcado: un proyecto que se abre y se
576
+ * cierra sin decir nada no deja una sesión vacía en el índice.
577
+ *
578
+ * Un ejecutor de PEGA (`--guion`, sin `crearEjecutor`) no vuelca NADA: el `.jsonl` de
579
+ * una sesión es historia del proyecto, y un transcript de pega indistinguible de uno
580
+ * real la ensuciaría — mañana alguien la reabre, lee la respuesta guionizada y no
581
+ * puede saber si fue el agente de pega o un fallo del de verdad. La decisión la toma
582
+ * la marca `ES_DOBLE` sobre `ejecutarTurnoGuionizado` (`cli/consola.ts`), nunca un
583
+ * booleano de este cierre ni la bandera `--guion` del argv: la misma razón por la que
584
+ * el resto del repo marca los dobles con un Symbol y no con un campo que alguien puede
585
+ * olvidar o poner mal. La guarda vive AQUÍ, en el único sitio que escribe a disco, y no
586
+ * repartida en cada llamador — `ejecutarTurno` de abajo y el `cerrar()` de más abajo
587
+ * comparten la misma `volcar`, así que ninguno de los dos puede colarse sin ella.
588
+ */
589
+ const volcar = () => {
590
+ if (esDoble(ejecutorEfectivo))
591
+ return;
592
+ const todos = consolaWeb.actos();
593
+ if (todos.length <= volcados)
594
+ return;
595
+ if (!anotada) {
596
+ sesiones.crear(raiz, idSesion, tarea);
597
+ anotada = true;
598
+ // Ahora sí hay una sesión en el índice a la que apuntar la foto de la apertura.
599
+ // Se guarda la promesa en vez de tirarla: `cerrar()` la espera. Ver `marcado`.
600
+ marcado = foto?.then((apuntar) => apuntar?.(idSesion));
601
+ }
602
+ for (const acto of todos.slice(volcados))
603
+ sesiones.anotar(raiz, idSesion, acto);
604
+ volcados = todos.length;
605
+ // El dispositivo elegido ANTES de que la sesión estuviera en el índice se anota
606
+ // ahora, que es la primera vez que hay una entrada a la que apuntarlo. Sin esto,
607
+ // elegir dispositivo y hablar después perdía la elección al reabrir.
608
+ if (dispositivo !== undefined)
609
+ sesiones.elegirDispositivo?.(raiz, idSesion, dispositivo);
610
+ };
611
+ /**
612
+ * El mensaje del commit del turno.
613
+ *
614
+ * Lleva el TÍTULO de la sesión, que es el asidero con el que una persona la reconoce en
615
+ * la barra, y el id de la tarea cuando la escribió una: es lo que permite leer el
616
+ * historial y saber qué salió de algo que corría solo.
617
+ *
618
+ * Lo que NO dice es «lo escribió el agente». El commit barre todo lo que haya cambiado,
619
+ * incluido lo que tocara una persona a mano en esa carpeta durante el turno — la misma
620
+ * honestidad que separa `Tarea.autorizadas` de «aplicados».
621
+ */
622
+ const mensajeDeCommit = () => {
623
+ const titulo = sesiones.listar(raiz).find((e) => e.id === idSesion)?.titulo;
624
+ return `xonecode: ${titulo === undefined || titulo === "" ? "sesión sin título" : titulo}${tarea === undefined ? "" : ` [tarea ${tarea}]`}`;
625
+ };
626
+ const ejecutarTurno = async (peticion, estado, consola) => {
627
+ /**
628
+ * La raíz del estado tiene que ser la de ESTA consola, y aquí se comprueba.
629
+ *
630
+ * Desde que `ejecutarTurno` se expone (`ConsolaDeProyecto.ejecutarTurno`, para las
631
+ * tareas), el estado lo puede construir quien llama — y de `estado.raiz` sale el
632
+ * backend del agente (`crearEjecutorReal` → `abrirSesionReal` → `backendDeAgente`).
633
+ * Una raíz equivocada no da error: hace que el agente lea y ESCRIBA en otro proyecto,
634
+ * con la aprobación de ese otro y sin un solo síntoma. Falla cerrado, como todo lo
635
+ * que decide sobre escrituras.
636
+ *
637
+ * Por la puerta de las personas esto no puede saltar nunca: el estado lo lleva
638
+ * `correrConsola` y ningún comando cambia `raiz` (medido en `cli/consola.ts`: solo
639
+ * `fuentes`, `seleccionesDeCatalogo` y `hilo`). Por eso la guarda va en el envoltorio
640
+ * COMPARTIDO y no solo en la puerta nueva — una guarda que solo vigila una puerta es
641
+ * la divergencia que esto viene a evitar.
642
+ */
643
+ if (estado.raiz !== raiz) {
644
+ throw new Error("ese turno trae la raíz de otro proyecto: esta consola solo corre turnos sobre la suya");
645
+ }
646
+ // El primer turno NUEVO es lo que deja de ser histórica una sesión reabierta.
647
+ // `sesiones.ts` no puede hacerlo —`reabrirSesion` es una lectura pura sin estado
648
+ // entre llamadas—, así que lo hace quien es dueño de la sesión viva, que es esto.
649
+ historica = false;
650
+ // Este envoltorio es el ÚNICO sitio que sabe cuándo empieza y cuándo acaba un turno:
651
+ // `correrConsola` solo lo espera y la piel solo ve eventos. De aquí sale lo que apaga
652
+ // el compositor, saca el botón de parar y enciende el borde vivo.
653
+ turnoEnVuelo = true;
654
+ consolaWeb.turno(true);
655
+ alFlancoDeTurno?.(true);
656
+ try {
657
+ // Se DEVUELVE lo que el turno informe. Sin este `return`, el canal que
658
+ // `crearEjecutorReal` acaba de abrir moría en este envoltorio: el corredor de
659
+ // tareas no tendría con qué medir si lo hecho es entregable, y ninguna piel se
660
+ // enteraría — que es exactamente cómo el `terminada` falso pasó desapercibido.
661
+ return await ejecutorEfectivo(peticion, estado, consola);
662
+ }
663
+ finally {
664
+ // En el `finally`: un turno que revienta o que se cancela también TERMINA, y dejar
665
+ // el compositor apagado para siempre sería peor que no haberlo apagado nunca.
666
+ turnoEnVuelo = false;
667
+ consolaWeb.turno(false);
668
+ alFlancoDeTurno?.(false);
669
+ volcar();
670
+ // Y el commit, DESPUÉS de volcar: así la entrada del índice ya existe y su título
671
+ // —que es lo que da nombre al commit— es el de verdad.
672
+ //
673
+ // Se ESPERA, al contrario que la ref de la foto: medido sobre una copia del
674
+ // proyecto real del usuario (165 MB, 41 colecciones), `add -A` + `commit` tardan
675
+ // 74 ms, y quien llama acaba de esperar un turno entero. Y hace falta esperarlo: la
676
+ // puerta de entrega del corredor corre en cuanto el turno devuelve y le pregunta a
677
+ // git, así que un commit suelto correría contra ella.
678
+ //
679
+ // Envuelto entero: esto va en el `finally` del turno, y una excepción aquí se
680
+ // llevaría por delante el cierre —el compositor apagado para siempre—. Un fallo se
681
+ // DICE, que es lo contrario de dejar el árbol sucio en silencio hasta que
682
+ // `/sync subir` se niegue.
683
+ try {
684
+ // El id de sesión va APARTE del mensaje: de él sale el sello con el que después
685
+ // se atribuye el cambio (`agent/sesionGit.ts`), y el título que va en el asunto no
686
+ // identifica nada — dos sesiones se pueden llamar igual, y renombrar una lo cambia.
687
+ const aviso = await opciones.commitearTurno?.(raiz, mensajeDeCommit(), idSesion);
688
+ if (aviso !== undefined)
689
+ informar(aviso);
690
+ }
691
+ catch (error) {
692
+ informar(`no se pudo commitear el turno: ${error.message}`);
693
+ }
694
+ }
695
+ };
696
+ // El estado con el que arranca el lazo, guardado ANTES de arrancarlo: es lo que
697
+ // `estadoDeSesion` devuelve mientras nadie haya cambiado nada, y sin él quien pinte el
698
+ // modelo en vigor no tendría qué enseñar hasta el primer `/modelo` — que es justo lo
699
+ // que se quiere evitar (enseñar «no se sabe» cuando sí se sabe).
700
+ let estadoDeSesion = {
701
+ // El hilo ES el id de la sesión: es lo que permite que reabrirla continúe la
702
+ // conversación en vez de releerla. `/nuevo` sigue pudiendo cambiarlo — abre otro hilo
703
+ // dentro de la misma sesión, y eso es exactamente lo que dice que hace.
704
+ hilo: idSesion,
705
+ raiz,
706
+ // Se LEE aquí, una vez por consola y no una por proceso: es lo que hace que una
707
+ // sesión nueva vea el `config.json` de ahora. Ver `OpcionesDelVestibulo.fuentes`.
708
+ fuentes: opciones.fuentes?.() ?? {},
709
+ };
710
+ // `/modelo` y `/modelos` cambian el modelo EN CALIENTE y no tocan disco, así que esta
711
+ // es la única forma de enterarse. Ver `Consola.alEstado`.
712
+ consolaWeb.consola.alEstado = (nuevo) => {
713
+ estadoDeSesion = nuevo;
714
+ // Al cable solo por la puerta de las personas: la escucha reemite el estado de
715
+ // modelos a los navegadores conectados, y el modelo de una tarea no es el que está
716
+ // en vigor para quien mira. El estado LOCAL se actualiza igual — `/modelo` dentro de
717
+ // la tarea tiene que surtir efecto en la tarea.
718
+ if (enElCable())
719
+ escuchaDeEstado?.(nuevo);
720
+ };
721
+ const terminada = correr(consolaWeb.consola, estadoDeSesion, ejecutarTurno).finally(() => {
722
+ cerrada = true;
723
+ });
724
+ // Un lazo que revienta sin que nadie espere `terminada` sería un rechazo sin manejar
725
+ // que tumba el proceso. Se cuenta y se sigue: la consola del vestíbulo sigue viva.
726
+ terminada.catch((error) => {
727
+ informar(`la consola del proyecto terminó con un error: ${error instanceof Error ? error.message : String(error)}`);
728
+ });
729
+ const consolaDeProyecto = {
730
+ raiz,
731
+ /**
732
+ * Lo que lleva consumido su sesión, contando lo de ANTES de este proceso.
733
+ *
734
+ * **Se PREGUNTA a la sesión, no se cachea**: el acumulador vive en el cierre de
735
+ * `abrirSesionReal` y es la única fuente; una copia aquí se quedaría vieja entre
736
+ * avisos. Y se le SUMA la base histórica leída al abrir, que es lo que hace que
737
+ * reabrir una conversación no ponga el contador a cero: sin esto, una sesión volvía
738
+ * sin ninguna cifra hasta su primer turno nuevo.
739
+ *
740
+ * `sesionReal` llega TARDE —`crearEjecutorReal` la anuncia dentro del primer turno—,
741
+ * así que antes de eso solo hay base, y si tampoco la hay se contesta `undefined`, que
742
+ * es «no consta» y hace que no se pinte nada.
743
+ */
744
+ consumo: () => {
745
+ const vivo = consumoVivo();
746
+ if (vivo === undefined) {
747
+ return consumoHistorico === undefined ? undefined : consumoDeLaSesion(consumoHistorico);
748
+ }
749
+ if (consumoHistorico === undefined)
750
+ return consumoDeLaSesion(vivo);
751
+ return consumoDeLaSesion(sumarConsumo(consumoHistorico, vivo));
752
+ },
753
+ get estadoDeSesion() {
754
+ return estadoDeSesion;
755
+ },
756
+ // Ver `ConsolaDeProyecto.idDeHilo`: este es el id crudo, sin esperar al índice.
757
+ idDeHilo: idSesion,
758
+ get sesion() {
759
+ // Solo cuando está en el ÍNDICE. El id existe desde que se abre —es el hilo—, pero
760
+ // hacia fuera «hay sesión» significa «hay una entrada que la barra puede marcar»:
761
+ // devolver un uuid que no está en ninguna lista haría que la barra buscara una fila
762
+ // que no existe, y que Revisión dijera que la sesión ya empezó sin haber empezado.
763
+ return anotada ? idSesion : undefined;
764
+ },
765
+ get historica() {
766
+ return historica;
767
+ },
768
+ trabajoAlAbrir,
769
+ get dispositivo() {
770
+ return dispositivo;
771
+ },
772
+ elegirDispositivo: (elegido) => {
773
+ dispositivo = elegido;
774
+ // Si todavía no está en el índice, se queda en memoria: `volcar()` lo anotará en
775
+ // cuanto cree la entrada. Escribir aquí una entrada nueva la enseñaría en la barra
776
+ // como una sesión vacía que nadie ha empezado.
777
+ if (anotada)
778
+ sesiones.elegirDispositivo?.(raiz, idSesion, elegido);
779
+ },
780
+ get cerrada() {
781
+ return cerrada;
782
+ },
783
+ get turnoEnVuelo() {
784
+ return turnoEnVuelo;
785
+ },
786
+ consola: consolaWeb,
787
+ cancelarTurno: () => {
788
+ if (sesionReal?.cancelar === undefined)
789
+ return false;
790
+ sesionReal.cancelar();
791
+ return true;
792
+ },
793
+ recibir: (mensaje) => {
794
+ consolaWeb.recibir(mensaje);
795
+ /**
796
+ * Una PROSA del usuario da de alta la sesión en el índice AHORA, no al acabar el
797
+ * turno.
798
+ *
799
+ * `volcar()` era lo único que creaba la entrada, y corre en la frontera del turno:
800
+ * así que la conversación no aparecía en la barra hasta que el asistente terminaba
801
+ * de contestar —minutos— y hasta entonces no había fila que marcar, ni que enseñar
802
+ * trabajando, ni que volver a abrir. Lo dijo el usuario mirando la pantalla.
803
+ *
804
+ * No contradice la pereza que la entrada tenía: esa existía para que «alguien mira
805
+ * un proyecto y se va sin decir nada» no dejara una sesión vacía en la barra, y
806
+ * mandar un mensaje es exactamente decir algo. Lo que sigue siendo perezoso es lo
807
+ * mismo de antes: abrir no escribe nada.
808
+ *
809
+ * **Solo con el turno parado**, y eso es la otra mitad. `volcar` no puede correr a
810
+ * mitad de turno: el último acto de la piel todavía muta —el cierre de una racha de
811
+ * tools SUSTITUYE a su apertura dentro del mismo acto— y el `.jsonl` solo sabe
812
+ * anexar, así que guardaría las dos líneas. Con el turno en vuelo no hace falta:
813
+ * la entrada ya la creó la prosa que lo arrancó. Y el acto de usuario en sí no muta
814
+ * nunca, que es lo que hace seguro volcarlo suelto.
815
+ */
816
+ if (mensaje.clase === "prosa" && !turnoEnVuelo)
817
+ volcar();
818
+ },
819
+ // El transcript del cliente lleva PRIMERO lo releído y después lo de esta ejecución.
820
+ // Lo releído no vuelve a pasar por `volcar`: ya está en el `.jsonl`.
821
+ conectar: (enviar) => [...(reabierta?.actos ?? []), ...consolaWeb.conectar(enviar)],
822
+ desconectar: (enviar) => consolaWeb.desconectar(enviar),
823
+ soltar: (enviar) => consolaWeb.soltar(enviar),
824
+ mensajesDeAprobacion: () => consolaWeb.mensajesDeAprobacion(),
825
+ actos: () => [...(reabierta?.actos ?? []), ...consolaWeb.actos()],
826
+ cerrar: async () => {
827
+ // El orden es el que evita que cerrar cuelgue: `consolaWeb.cerrar()` pone el EOF,
828
+ // pero `correrConsola` no lo mira hasta que el turno en vuelo devuelve, y un turno
829
+ // en vuelo puede tardar minutos. Abortar la sesión real primero es lo que lo
830
+ // desbloquea — y es para lo que existe `SesionReal.cerrar()`.
831
+ cerrando = true;
832
+ sesionReal?.cerrar();
833
+ consolaWeb.cerrar();
834
+ await terminada.catch(() => 0);
835
+ volcar();
836
+ },
837
+ /**
838
+ * Espera a que la ref de la sesión esté ESCRITA, para quien vaya a mirarla.
839
+ *
840
+ * Estuvo un rato dentro de `cerrar()` y era una regresión para quien está sentado
841
+ * delante: cerrar un proyecto pasaba a aguardar un `git add -A` + `write-tree` sobre
842
+ * el árbol entero, o sea una pausa visible en un proyecto grande — y por un camino
843
+ * que no la necesita. Quien la necesita es el CORREDOR de tareas, que mide la
844
+ * condición de «revisable» justo después de cerrar; así que la espera la hace él y no
845
+ * se le cobra a nadie más. Ver `marcado`.
846
+ */
847
+ esperarMarca: async () => void (await marcado?.catch(() => undefined)),
848
+ // El MISMO objeto que se le acaba de pasar a `correr`, no un segundo envoltorio: dos
849
+ // versiones del turno divergen en la primera corrección que solo toque a una.
850
+ ejecutarTurno,
851
+ terminada,
852
+ };
853
+ return consolaDeProyecto;
854
+ };
855
+ const abrirDeVerdad = async (apertura) => {
856
+ /**
857
+ * ANTES de cerrar nada: si esa conversación es la de una tarea en curso, no se abre.
858
+ *
859
+ * Es la MISMA guarda que `borrarSesion`, con el mismo motivo de fondo —el hilo lo está
860
+ * escribiendo alguien— y sobre la misma fuente (`deTareas`, que es quien sabe qué
861
+ * consolas están vivas). Lo que pasaba sin ella: el `thread_id` del checkpointer ES el id
862
+ * de la sesión, así que abrirla aquí ponía DOS consolas escribiendo el mismo hilo, con el
863
+ * agente a mitad de turno en una de ellas. El botón existía desde siempre (el título de
864
+ * la tarjeta del kanban), y con «Ver lo que hace» a su lado un botón correcto pegado a uno
865
+ * peligroso enseña que los dos son igual de seguros.
866
+ *
867
+ * **El orden es la mitad del arreglo**: abajo se cierra la consola que ocupe esa raíz,
868
+ * así que una guarda puesta después habría cerrado la sesión de la persona para después
869
+ * negarse a abrir la otra — dos daños en vez de ninguno.
870
+ *
871
+ * Se compara con `sesion` Y con `idDeHilo` porque son dos momentos de la misma
872
+ * conversación: `sesion` no existe hasta que se vuelca el primer acto, y `idDeHilo` desde
873
+ * el primer instante. El que viaja en la tarjeta es el primero; el que cubre la ventana
874
+ * anterior al volcado es el segundo.
875
+ */
876
+ if (apertura.sesion !== undefined && esDeUnaTareaEnCurso(apertura.raiz, apertura.sesion)) {
877
+ throw new Error(MOTIVO_SESION_DE_TAREA_EN_CURSO);
878
+ }
879
+ // Poda: una consola cuyo lazo ya terminó por su cuenta (un `/salir`, un fallo del lazo)
880
+ // no puede seguir ocupando su raíz. Es la misma poda perezosa que `deTareas`, y aquí es
881
+ // lo que impide que un proyecto quede inabrible para siempre.
882
+ for (const [raiz, vieja] of [...abiertas])
883
+ if (vieja.cerrada)
884
+ abiertas.delete(raiz);
885
+ const yaAbierta = abiertas.get(apertura.raiz);
886
+ if (yaAbierta !== undefined) {
887
+ /**
888
+ * La MISMA sesión de un proyecto que ya está abierto: solo se muda el foco.
889
+ *
890
+ * Es el caso corriente de volver a ella desde la barra, y cerrarla para reabrirla
891
+ * sería tirar el turno que quizá está corriendo — justo lo que este cambio existe para
892
+ * evitar. Se compara con `sesion` Y con `idDeHilo` por lo mismo que la guarda de las
893
+ * tareas: son dos momentos de la misma conversación, y el segundo cubre la ventana
894
+ * anterior al primer volcado.
895
+ */
896
+ const laMisma = apertura.sesion !== undefined &&
897
+ (yaAbierta.sesion === apertura.sesion || yaAbierta.idDeHilo === apertura.sesion);
898
+ /**
899
+ * Abrir el PROYECTO —sin nombrar sesión— que está trabajando es VOLVER a lo que está
900
+ * pasando, no pedir una conversación nueva.
901
+ *
902
+ * Y es la única forma de llegar: el id de una sesión nace al volcar su primer acto, o
903
+ * sea al final del turno, así que la conversación que acabas de arrancar no tiene fila
904
+ * en la barra todavía. Medido en la pantalla del usuario: arrancó una sesión, se fue,
905
+ * pulsó el nombre del proyecto para ver qué hacía y se llevó el rechazo cinco veces —
906
+ * el botón muerto de siempre, y sin ninguna otra puerta detrás.
907
+ *
908
+ * La distinción con «nueva sesión» no la hace esto, la hace el cliente: el «+» se
909
+ * apaga mientras el proyecto trabaja, porque una segunda conversación sobre la misma
910
+ * copia de trabajo sigue siendo imposible. Lo que queda aquí es que el gesto de
911
+ * «llévame a este proyecto» lleve a alguna parte.
912
+ */
913
+ const volverALoQueTrabaja = apertura.sesion === undefined && yaAbierta.turnoEnVuelo;
914
+ if (laMisma || volverALoQueTrabaja) {
915
+ enFoco = apertura.raiz;
916
+ await cerrarLasOciosasSalvo(apertura.raiz);
917
+ return yaAbierta;
918
+ }
919
+ /**
920
+ * OTRA sesión del MISMO proyecto, con la que hay trabajando: se DECLINA.
921
+ *
922
+ * Una consola por raíz no es una limitación de este mapa: es la copia de trabajo. Dos
923
+ * conversaciones escribiendo los mismos ficheros no tienen aislamiento de ninguna
924
+ * clase —es el mismo motivo por el que una tarea no arranca donde hay una persona— y
925
+ * cerrar la que trabaja para abrir la otra sería la interrupción que se acaba de
926
+ * quitar. Si está OCIOSA sí se cierra y se abre la nueva, que es lo de siempre.
927
+ */
928
+ if (yaAbierta.turnoEnVuelo)
929
+ throw new Error(motivoDeProyectoTrabajando());
930
+ await cerrarConsolaDeProyecto(apertura.raiz);
931
+ }
932
+ const raiz = apertura.raiz;
933
+ await cerrarLasOciosasSalvo(raiz);
934
+ /**
935
+ * El foco se muda ANTES de construir, y no después.
936
+ *
937
+ * No es orden por gusto: el lazo arranca DENTRO de `construirConsolaDeProyecto`, así que
938
+ * su primer flanco de turno puede salir de ahí —lo hace con cualquier `correr` que no
939
+ * espere una línea—, y con el foco puesto después ese flanco habría hablado de la
940
+ * consola anterior. Que quede apuntando a una raíz sin consola si la construcción
941
+ * revienta es inocuo: `consolaEnFoco()` pregunta al mapa, y ahí no habrá nada.
942
+ */
943
+ enFoco = raiz;
944
+ const consolaDeProyecto = await construirConsolaDeProyecto({
945
+ ...apertura,
946
+ enElCable: () => enFoco === raiz,
947
+ alFlancoDeTurno: (activo) => {
948
+ // La barra se repinta en los DOS flancos y esté o no en foco: de aquí sale que una
949
+ // sesión de segundo plano está trabajando.
950
+ avisarDeLasAbiertas({ raiz, turnoEnVuelo: activo });
951
+ // Y al terminar, si ya no la mira nadie, se cierra. Ver `cerrarSiSobra`.
952
+ if (!activo)
953
+ cerrarSiSobra(raiz);
954
+ },
955
+ });
956
+ abiertas.set(raiz, consolaDeProyecto);
957
+ return consolaDeProyecto;
958
+ };
959
+ /**
960
+ * Las consolas abiertas para tareas. No son `abierto` —nadie las mira— pero cada una
961
+ * tiene su `correrConsola` vivo, así que `cerrar()` tiene que llevárselas: un lazo que
962
+ * nadie cierra deja el proceso sin terminar, y «cierra el propio vestíbulo» sería falso.
963
+ *
964
+ * El objeto se guarda TAL CUAL, sin envolver su `cerrar`: `ConsolaDeProyecto` expone
965
+ * captadores (`sesion`, `historica`, `cerrada`) y una copia con `...` los congelaría en
966
+ * el valor que tuvieran al abrir — `sesion` se quedaría en `undefined` para siempre. Las
967
+ * ya cerradas se PODAN al abrir la siguiente, que es cuando puede empezar a crecer.
968
+ */
969
+ const deTareas = new Set();
970
+ /**
971
+ * ¿Esa conversación la está escribiendo una tarea de fondo AHORA?
972
+ *
973
+ * La fuente es `deTareas` y no el índice de tareas a propósito: lo que hace daño es que
974
+ * haya dos consolas de ESTE proceso sobre el mismo hilo, y eso solo lo sabe este conjunto.
975
+ * Una tarea `en-proceso` en el OTRO proceso no aparece aquí, y esa limitación es la misma
976
+ * que ya declara `borrarSesion` — el límite lo pone el sistema operativo, no esta función.
977
+ */
978
+ const esDeUnaTareaEnCurso = (raiz, sesion) => [...deTareas].some((consola) => consola.raiz === raiz && !consola.cerrada && (consola.sesion === sesion || consola.idDeHilo === sesion));
979
+ const abrirParaTarea = async (raiz, sesion, adjuntos, tarea) => {
980
+ // ANTES de construir nada: sin esto, una raíz equivocada dejaba un `correrConsola`
981
+ // vivo, una foto de git lanzada y un id de hilo gastado. Y el motivo no lleva la ruta
982
+ // —puede ser la del home— porque de aquí el error sube al registro de la tarea.
983
+ if (!esProyecto(raiz)) {
984
+ throw new Error("esa raíz no es un proyecto de xonecode: falta su .xonecode/config.json");
985
+ }
986
+ for (const vieja of deTareas)
987
+ if (vieja.cerrada)
988
+ deTareas.delete(vieja);
989
+ // `sesion` reenviada tal cual: es lo que hace que `construirConsolaDeProyecto` REABRA
990
+ // ese hilo —`idSesion = sesion ?? randomUUID()`— en vez de abrir uno nuevo. `undefined`
991
+ // en la primera ejecución de la tarea sigue dando una sesión nueva, como siempre.
992
+ // `adjuntos` reenviada igual que `sesion`, y por el mismo motivo: quien la conoce es el
993
+ // corredor, que es el único que sabe de qué tarea es esta apertura.
994
+ // `tarea` viaja por lo mismo que las otras dos: quien sabe de qué tarea es esta
995
+ // apertura es el corredor. Es lo que marca su fila en el índice como sesión de tarea.
996
+ const consolaDeProyecto = await construirConsolaDeProyecto({
997
+ raiz,
998
+ sesion,
999
+ adjuntos,
1000
+ tarea,
1001
+ // Nunca. Una consola de tarea no está en el cable ni puede llegar a estarlo: el foco
1002
+ // es de las de persona, y su raíz podría coincidir con una de ellas.
1003
+ enElCable: () => false,
1004
+ });
1005
+ deTareas.add(consolaDeProyecto);
1006
+ return consolaDeProyecto;
1007
+ };
1008
+ /** Cierra las consolas de tarea que queden vivas. Ver `deTareas`. */
1009
+ const cerrarLasDeTareas = async () => {
1010
+ for (const consola of [...deTareas]) {
1011
+ deTareas.delete(consola);
1012
+ // Una que ya cerró el corredor no se vuelve a cerrar, y un fallo al cerrar una no
1013
+ // puede dejar a las demás vivas: el cierre es lo último que corre.
1014
+ if (!consola.cerrada)
1015
+ await consola.cerrar().catch(() => undefined);
1016
+ }
1017
+ };
1018
+ return {
1019
+ consola: consolaDelVestibulo,
1020
+ nombre: opciones.nombre,
1021
+ async pasosPendientes() {
1022
+ const pasos = [];
1023
+ // La MISMA condición que usa `asistenteDeModelo`: si `trabajo` no resolvió por
1024
+ // `omision`, alguien eligió (proveedor, modelo, clave si el proveedor la pide,
1025
+ // global o bandera) y no se pregunta.
1026
+ if (opciones.origenDeTrabajo === "omision")
1027
+ pasos.push("cuenta");
1028
+ if (registrados.length === 0)
1029
+ pasos.push("entorno");
1030
+ // El proyecto YA NO es un paso del alta — cambio de rumbo del usuario: con cuenta
1031
+ // y entorno resueltos se entra directo al dashboard, y el proyecto se elige en la
1032
+ // barra lateral (entorno → proyectos → sesiones), no aquí. `abierto` sigue
1033
+ // existiendo como estado (`proyectoAbierto()`, más abajo) pero no bloquea nada.
1034
+ return pasos;
1035
+ },
1036
+ alCambiarEstadoDeSesion(escucha) {
1037
+ escuchaDeEstado = escucha;
1038
+ },
1039
+ alCambiarTurno(escucha) {
1040
+ escuchaDeTurno = escucha;
1041
+ },
1042
+ alCambiarConsumo(escucha) {
1043
+ escuchaDeConsumo = escucha;
1044
+ },
1045
+ // La del FOCO, igual que `proyectoAbierto()`: es la sesión que se está mirando.
1046
+ consumoDeSesion: () => consolaEnFoco()?.consumo?.(),
1047
+ opcionesDeEntorno: () => [...ENTORNOS_OFICIALES, ENTORNO_OTRO],
1048
+ entornosRegistrados: () => [...registrados],
1049
+ async pasoDeCuenta() {
1050
+ // Sigue siendo `asistenteDeModelo` con la consola web detrás y nada más. Lo único que
1051
+ // esta piel añade es `exigirEleccion`: aquí el paso de cuenta es la PUERTA del
1052
+ // dashboard, así que cancelar vuelve a preguntar en vez de dejar entrar con el modelo
1053
+ // por omisión. El asistente decide cuándo parar mirando `eof()` — sin cliente, no hay
1054
+ // a quién insistirle.
1055
+ return asistenteDeModelo(consolaDelVestibulo.consola, {
1056
+ origenDeTrabajo: opciones.origenDeTrabajo,
1057
+ hayCredencial: opciones.hayCredencial ?? (() => false),
1058
+ guardarCredencial: registrarCredencial,
1059
+ // La clave se prueba contra el catálogo ANTES de escribirse; esto es lo que la
1060
+ // pone en el proceso mientras tanto. Entra por opción como todo lo que toca el
1061
+ // sistema: un test no puede escribir en el `process.env` del que corre los tests
1062
+ // sin decirlo.
1063
+ ...(opciones.aplicarCredencial === undefined ? {} : { aplicarCredencial: opciones.aplicarCredencial }),
1064
+ exigirEleccion: true,
1065
+ });
1066
+ },
1067
+ async guardarCredencialDe(proveedor, clave) {
1068
+ const guardada = registrarCredencial(proveedor, clave);
1069
+ // Se dice AQUÍ, en el momento: si el usuario cancela el paso siguiente, un
1070
+ // «cancelado» a secas daría a entender que no se tocó nada, y la clave ya está en
1071
+ // disco. Es la misma frase que escribe `/provider` y el asistente de terminal.
1072
+ informar(`credencial de ${proveedor} guardada en ${guardada.ruta}`);
1073
+ return guardada;
1074
+ },
1075
+ async registrarEntorno(entorno) {
1076
+ if (!urlDeEntornoValida(entorno.url)) {
1077
+ throw new Error(AVISO_DE_URL_DE_MCP);
1078
+ }
1079
+ // Lo ÚNICO que el formulario pide es la URL; la identidad se deduce de ella.
1080
+ const identificado = identidadDeEntorno(entorno);
1081
+ const guardado = opciones.guardarEntorno(identificado);
1082
+ // Justo después de registrar, y no antes: `adoptarLegadoSiProcede` solo actúa si la
1083
+ // URL es la oficial por omisión, que es la única que el fichero plano pudo usar.
1084
+ adoptarLegado(identificado);
1085
+ // SUSTITUYE por id, como hace `settingsEnDisco.guardarEntorno` en el fichero. Con un
1086
+ // `push` a secas, registrar dos veces el mismo entorno —cosa que la web hace cada vez
1087
+ // que alguien lo elige— dejaba dos entradas para un id, y `entornoPorId` resolvía a la
1088
+ // primera: la vieja. Registrar es idempotente aquí igual que en disco.
1089
+ const yaEstaba = registrados.findIndex((e) => e.id === identificado.id);
1090
+ if (yaEstaba >= 0)
1091
+ registrados.splice(yaEstaba, 1, identificado);
1092
+ else
1093
+ registrados.push(identificado);
1094
+ informar(`entorno «${identificado.id}» registrado en ${guardado.ruta}`);
1095
+ // El entorno YA identificado vuelve con la ruta: quien registró un «otro» no sabe con
1096
+ // qué id quedó, y `arranque.ts` necesita ese id exacto para pedirle los proyectos —
1097
+ // con el «otro» de la lista, `entornoPorId` no encontraría nada.
1098
+ return { ...guardado, entorno: identificado };
1099
+ },
1100
+ async guardarProyectosVisibles(entorno, proyectos) {
1101
+ const registrado = entornoPorId(entorno);
1102
+ const conProyectos = { ...registrado, proyectos: [...proyectos] };
1103
+ const guardado = opciones.guardarEntorno(conProyectos);
1104
+ const donde = registrados.findIndex((e) => e.id === registrado.id);
1105
+ if (donde >= 0)
1106
+ registrados.splice(donde, 1, conProyectos);
1107
+ informar(`proyectos visibles de «${registrado.id}»: ${proyectos.length === 0 ? "ninguno" : proyectos.join(", ")}`);
1108
+ return guardado;
1109
+ },
1110
+ async proyectosDe(entorno) {
1111
+ const registrado = entornoPorId(entorno);
1112
+ if (opciones.proyectosDeEntorno === undefined) {
1113
+ throw new Error("esta ejecución no tiene conexión con CloudStudio");
1114
+ }
1115
+ const { proyectos, servidor } = await opciones.proyectosDeEntorno(registrado);
1116
+ renombrarConElServidor(registrado, servidor?.nombre);
1117
+ return proyectos;
1118
+ },
1119
+ async ramasDe(entorno, proyecto) {
1120
+ // El NOMBRE, siempre: es lo único que el servidor sabe abrir.
1121
+ const nombre = typeof proyecto === "string" ? proyecto : proyecto.nombre;
1122
+ const registrado = entornoPorId(entorno);
1123
+ if (opciones.ramasDeProyecto === undefined) {
1124
+ throw new Error("esta ejecución no tiene conexión con CloudStudio");
1125
+ }
1126
+ return opciones.ramasDeProyecto(registrado, nombre);
1127
+ },
1128
+ raizDeProyecto(entorno, proyecto) {
1129
+ return rutaDeWorkspace(base, entornoPorId(entorno).id, proyecto);
1130
+ },
1131
+ sesionesDe(raiz) {
1132
+ // Una carpeta que no existe no es un fallo: es un proyecto que nunca se abrió. El
1133
+ // puerto se lo traga y devuelve vacío, que es la verdad.
1134
+ try {
1135
+ // Campo a campo y no la entrada entera: del índice cuelgan además `creada` y el
1136
+ // `dispositivo`, que la barra no pinta. Los tres que sí salen son los que distinguen
1137
+ // una fila de otra: CUÁNDO se tocó, de QUIÉN es y CUÁNTO costó.
1138
+ return sesiones.listar(raiz).map((s) => ({
1139
+ id: s.id,
1140
+ titulo: s.titulo,
1141
+ ...(s.ultimoTurno === undefined ? {} : { ultimoTurno: s.ultimoTurno }),
1142
+ ...(s.tarea === undefined ? {} : { tarea: s.tarea }),
1143
+ ...(s.consumo === undefined ? {} : { consumo: s.consumo }),
1144
+ }));
1145
+ }
1146
+ catch {
1147
+ return [];
1148
+ }
1149
+ },
1150
+ async borrarSesion(raiz, id) {
1151
+ // Antes de tocar NADA: si esa conversación es la de una tarea en curso, no se borra.
1152
+ // `olvidarMarcaDeSesion` y `olvidarMemoriaDeHilo` no son condicionales, así que
1153
+ // borrar aquí se habría llevado la ref de git y el checkpoint de un hilo que el
1154
+ // agente sigue escribiendo — y su siguiente `volcar()` habría devuelto la fila a la
1155
+ // barra, dejando al usuario con un botón que parece no funcionar. Ver `deTareas`.
1156
+ // El MISMO predicado que la guarda de `abrirDeVerdad`: dos copias de «¿la está
1157
+ // escribiendo una tarea?» acabarían discrepando, y la que se quedara vieja sería la
1158
+ // que deja pasar el daño.
1159
+ if (esDeUnaTareaEnCurso(raiz, id)) {
1160
+ return {
1161
+ borrada: false,
1162
+ cerroLaAbierta: false,
1163
+ motivo: "esa conversación es la de una tarea en curso: se podrá borrar cuando termine",
1164
+ };
1165
+ }
1166
+ // Cerrar ANTES de borrar: ver el comentario del contrato. Y solo si es ESA sesión —
1167
+ // cerrar el proyecto entero porque se borró una conversación vieja de la lista sería
1168
+ // llevarse por delante el trabajo en curso.
1169
+ // La consola de ESA sesión, si hay alguna abierta: puede no ser la que está en foco
1170
+ // desde que cambiar de sesión no mata la anterior. `cerroLaAbierta` sigue queriendo
1171
+ // decir «la que estabas mirando», que es lo que el cliente usa para volver al
1172
+ // escritorio: cerrar una de segundo plano no le cambia la vista a nadie.
1173
+ const suya = abiertas.get(raiz);
1174
+ const laDeEsaSesion = suya !== undefined && suya.sesion === id;
1175
+ const cerroLaAbierta = laDeEsaSesion && enFoco === raiz;
1176
+ if (laDeEsaSesion)
1177
+ await cerrarConsolaDeProyecto(raiz);
1178
+ const borrada = sesiones.borrar?.(raiz, id) ?? false;
1179
+ // La ref de git de la sesión se va con ella: apunta a un árbol que solo esa vista
1180
+ // usaba, y dejarla mantendría ese árbol vivo para siempre (`agent/sesionGit.ts`).
1181
+ // No es condicional al `borrada`: una entrada de índice ya perdida no es motivo para
1182
+ // dejar la ref colgada.
1183
+ await opciones.olvidarMarcaDeSesion?.(raiz, id);
1184
+ // Y su memoria: el `thread_id` es el id de la sesión, así que el checkpoint queda
1185
+ // huérfano y con el contenido de todo lo que se escribió en ella. Tampoco condicional
1186
+ // al `borrada`, por lo mismo que la ref.
1187
+ await opciones.olvidarMemoriaDeHilo?.(raiz, id);
1188
+ return { borrada, cerroLaAbierta };
1189
+ },
1190
+ renombrarSesion(raiz, id, titulo) {
1191
+ return sesiones.renombrar?.(raiz, id, titulo) ?? false;
1192
+ },
1193
+ async completarProyecto({ entorno, proyecto, rama }) {
1194
+ const registrado = entornoPorId(entorno);
1195
+ // El listado remoto trae `{id, nombre}`; un nombre suelto vale porque en CloudStudio
1196
+ // el proyecto se abre POR NOMBRE (`studio_open_project`) y el id solo identifica.
1197
+ //
1198
+ // Se DESESTRUCTURA, y no es estilo: lo que entre aquí acaba en el `config.json` del
1199
+ // proyecto (`guardarProyectoCloudStudioDeProyecto`, abajo), y quien llama le pasa una
1200
+ // fila del listado remoto — que hoy trae además `compartido` y mañana podría traer
1201
+ // otra cosa. TypeScript no avisa: la comprobación de propiedades de más solo salta
1202
+ // con un literal, no con un objeto que llega por variable. Copiar los dos campos que
1203
+ // SON la identidad es lo que impide que el disco se llene de datos del servidor.
1204
+ const identidad = typeof proyecto === "string"
1205
+ ? { id: proyecto, nombre: proyecto }
1206
+ : { id: proyecto.id, nombre: proyecto.nombre };
1207
+ const raiz = rutaDeWorkspace(base, registrado.id, identidad.nombre);
1208
+ const datos = {
1209
+ entorno: registrado.id,
1210
+ url: registrado.url,
1211
+ scopes: registrado.scopes ?? SCOPES_CLOUDSTUDIO_AGENTE,
1212
+ proyecto: identidad,
1213
+ rama,
1214
+ };
1215
+ // El alta se escribe ENTERA antes de bajar, y no después. No es un descuido: la
1216
+ // frase que se dice cuando la descarga falla es «reintenta con /sync bajar», y
1217
+ // `/sync` lee del disco el proyecto y la rama — sin el alta escrita, ese consejo
1218
+ // sería mentira. «No a medias» significa completa o nada, no «nada».
1219
+ const { ruta } = opciones.guardarConfigDeProyecto(raiz, datos);
1220
+ proyectoEscrito = { raiz, ruta };
1221
+ informar(`proyecto dado de alta en ${ruta}`);
1222
+ try {
1223
+ await opciones.descargar({ ...datos, raiz });
1224
+ }
1225
+ catch (error) {
1226
+ const detalle = error instanceof Error ? error.message : String(error);
1227
+ informar(`no se pudo descargar el proyecto: ${detalle}`);
1228
+ informar(`el alta quedó completa en ${ruta}; reintenta la descarga con «/sync bajar»`);
1229
+ // Se propaga: quien llama decide si vuelve al paso de proyecto o abre igualmente
1230
+ // la copia vacía. Tragárselo aquí dejaría creer que el proyecto está bajado.
1231
+ throw error;
1232
+ }
1233
+ return { raiz, ruta };
1234
+ },
1235
+ abrirProyecto: (apertura) => enCola(() => abrirDeVerdad(apertura)),
1236
+ /**
1237
+ * Abrir un proyecto para una TAREA: la misma construcción, sin registrarlo como el
1238
+ * proyecto abierto ni mudar el sumidero del cable.
1239
+ *
1240
+ * Existe porque el vestíbulo sirve UN proyecto a la vez (`proyectoAbierto()` devuelve
1241
+ * uno) y el cable se muda a esa consola (`arranque.ts#adjuntar`): si el corredor
1242
+ * reusara `abrirProyecto`, cada tarea que arrancase cerraría la consola de quien esté
1243
+ * trabajando y le movería la vista.
1244
+ *
1245
+ * Va por la misma COLA que la otra puerta aunque no toque `abierto`: la cola es lo que
1246
+ * sostiene «una consola de proyecto a la vez», y colarse por delante de un
1247
+ * un cierre a medias sería empezar a construir mientras el lazo anterior
1248
+ * todavía vive. El precio, dicho: una tarea que llega mientras se cierra un turno
1249
+ * humano de minutos espera a que acabe. Para trabajo de fondo es el lado correcto.
1250
+ */
1251
+ abrirParaTarea: (raiz, sesion, adjuntos, tarea) => enCola(() => abrirParaTarea(raiz, sesion, adjuntos, tarea)),
1252
+ proyectoAbierto: () => consolaEnFoco(),
1253
+ // La del foco INCLUIDA, y las cerradas fuera: una consola cuyo lazo ya terminó no ocupa
1254
+ // su raíz, y de esta lista cuelga la guarda de «gana la persona».
1255
+ proyectosAbiertos: () => [...abiertas.values()].filter((c) => !c.cerrada),
1256
+ cancelar: () => enCola(async () => {
1257
+ await cerrarLasDeProyecto();
1258
+ informar("alta cancelada");
1259
+ // Negar lo que ya está en disco sería mentir. Cancelar no BORRA nada: dice qué quedó.
1260
+ if (credencialEscrita !== undefined) {
1261
+ informar(`la credencial de ${credencialEscrita.proveedor} sigue escrita en ${credencialEscrita.ruta}`);
1262
+ }
1263
+ if (proyectoEscrito !== undefined) {
1264
+ informar(`el alta del proyecto sigue escrita en ${proyectoEscrito.ruta}`);
1265
+ }
1266
+ }),
1267
+ cerrar: () => enCola(async () => {
1268
+ await cerrarLasDeProyecto();
1269
+ // Y las de las tareas: cada una tiene su `correrConsola` vivo, y dejarlo corriendo
1270
+ // deja el proceso sin terminar. Ver `deTareas`.
1271
+ await cerrarLasDeTareas();
1272
+ consolaDelVestibulo.cerrar();
1273
+ }),
1274
+ };
1275
+ }
1276
+ //# sourceMappingURL=vestibulo.js.map