xonecode 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. package/README.md +49 -7
  2. package/THIRD_PARTY_NOTICES.md +128 -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 +70 -10
  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/tokenTracking.js +22 -1
  300. package/dist/vendor/tokenTracking.js.map +1 -1
  301. package/dist/web/servidor/arranque.js +3964 -0
  302. package/dist/web/servidor/arranque.js.map +1 -0
  303. package/dist/web/servidor/consolaDeTarea.js +161 -0
  304. package/dist/web/servidor/consolaDeTarea.js.map +1 -0
  305. package/dist/web/servidor/consolaWeb.js +389 -0
  306. package/dist/web/servidor/consolaWeb.js.map +1 -0
  307. package/dist/web/servidor/corredorDeTareas.js +1119 -0
  308. package/dist/web/servidor/corredorDeTareas.js.map +1 -0
  309. package/dist/web/servidor/pielWeb.js +269 -0
  310. package/dist/web/servidor/pielWeb.js.map +1 -0
  311. package/dist/web/servidor/servidor.js +257 -0
  312. package/dist/web/servidor/servidor.js.map +1 -0
  313. package/dist/web/servidor/sesiones.js +440 -0
  314. package/dist/web/servidor/sesiones.js.map +1 -0
  315. package/dist/web/servidor/transporte.js +169 -0
  316. package/dist/web/servidor/transporte.js.map +1 -0
  317. package/dist/web/servidor/vestibulo.js +1276 -0
  318. package/dist/web/servidor/vestibulo.js.map +1 -0
  319. package/package.json +30 -8
  320. package/skills/archify/LICENSE +22 -0
  321. package/skills/archify/SKILL.md +109 -0
  322. package/skills/archify/THIRD_PARTY_NOTICES.md +56 -0
  323. package/skills/archify/assets/template.html +14787 -0
  324. package/skills/archify/bin/archify.mjs +1990 -0
  325. package/skills/archify/bin/open-artifact.mjs +86 -0
  326. package/skills/archify/bin/preview.mjs +648 -0
  327. package/skills/archify/bin/visual-check.mjs +829 -0
  328. package/skills/archify/brand-marks/README.md +31 -0
  329. package/skills/archify/brand-marks/catalog.json +131 -0
  330. package/skills/archify/delta/architecture-delta.mjs +1214 -0
  331. package/skills/archify/examples/agent-run.lifecycle.json +71 -0
  332. package/skills/archify/examples/agent-tool-call.workflow.json +94 -0
  333. package/skills/archify/examples/async-job-roundtrip.sequence.json +61 -0
  334. package/skills/archify/examples/brand-aware-delivery.architecture.json +47 -0
  335. package/skills/archify/examples/cache-miss-request.sequence.json +81 -0
  336. package/skills/archify/examples/checkout-platform.base.architecture.json +31 -0
  337. package/skills/archify/examples/checkout-platform.head.architecture.json +31 -0
  338. package/skills/archify/examples/dataflow-product-analytics.html +14898 -0
  339. package/skills/archify/examples/deployment-release.lifecycle.json +49 -0
  340. package/skills/archify/examples/event-stream.dataflow.json +57 -0
  341. package/skills/archify/examples/incident-response.workflow.json +64 -0
  342. package/skills/archify/examples/lifecycle-agent-run.html +14847 -0
  343. package/skills/archify/examples/product-analytics.dataflow.json +76 -0
  344. package/skills/archify/examples/production-deployment.architecture.json +71 -0
  345. package/skills/archify/examples/release-delivery.workflow.json +62 -0
  346. package/skills/archify/examples/sequence-cache-miss-request.html +14913 -0
  347. package/skills/archify/examples/web-app-rendered.html +14862 -0
  348. package/skills/archify/examples/web-app.architecture.json +46 -0
  349. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +14904 -0
  350. package/skills/archify/migrations/workflow-v2.mjs +279 -0
  351. package/skills/archify/package.json +17 -0
  352. package/skills/archify/recipes/scenarios.mjs +391 -0
  353. package/skills/archify/references/authoring-contract.md +198 -0
  354. package/skills/archify/references/brand-marks.md +69 -0
  355. package/skills/archify/references/delivery-contract.md +116 -0
  356. package/skills/archify/references/viewer-runtime.md +45 -0
  357. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  358. package/skills/archify/renderers/architecture/render-architecture.mjs +1078 -0
  359. package/skills/archify/renderers/dataflow/README.md +104 -0
  360. package/skills/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  361. package/skills/archify/renderers/lifecycle/README.md +115 -0
  362. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  363. package/skills/archify/renderers/sequence/README.md +114 -0
  364. package/skills/archify/renderers/sequence/render-sequence.mjs +453 -0
  365. package/skills/archify/renderers/shared/brand-marks.mjs +563 -0
  366. package/skills/archify/renderers/shared/cli.mjs +218 -0
  367. package/skills/archify/renderers/shared/desktop-readability.mjs +26 -0
  368. package/skills/archify/renderers/shared/diagnostics.mjs +127 -0
  369. package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
  370. package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  371. package/skills/archify/renderers/shared/generated-validators.mjs +13 -0
  372. package/skills/archify/renderers/shared/geometry.mjs +1423 -0
  373. package/skills/archify/renderers/shared/i18n.mjs +594 -0
  374. package/skills/archify/renderers/shared/layout-report.mjs +40 -0
  375. package/skills/archify/renderers/shared/legend.mjs +217 -0
  376. package/skills/archify/renderers/shared/output-path.mjs +321 -0
  377. package/skills/archify/renderers/shared/repository-evidence.mjs +235 -0
  378. package/skills/archify/renderers/shared/text-fit.mjs +49 -0
  379. package/skills/archify/renderers/shared/utils.mjs +232 -0
  380. package/skills/archify/renderers/shared/validator.mjs +86 -0
  381. package/skills/archify/renderers/workflow/README.md +223 -0
  382. package/skills/archify/renderers/workflow/render-workflow.mjs +35 -0
  383. package/skills/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  384. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  385. package/skills/archify/schemas/README.md +211 -0
  386. package/skills/archify/schemas/architecture.schema.json +176 -0
  387. package/skills/archify/schemas/common.schema.json +115 -0
  388. package/skills/archify/schemas/dataflow.schema.json +243 -0
  389. package/skills/archify/schemas/lifecycle.schema.json +266 -0
  390. package/skills/archify/schemas/sequence.schema.json +223 -0
  391. package/skills/archify/schemas/workflow.schema.json +428 -0
  392. package/skills/archify/scripts/check-render-output.mjs +835 -0
  393. package/skills/archify/scripts/check-update.mjs +1667 -0
  394. package/skills/archify/scripts/render-examples.mjs +26 -0
  395. package/skills/archify/scripts/update-contract.mjs +182 -0
  396. package/skills/archify/skill-release.json +10 -0
  397. package/skills/artifacts-builder/SKILL.md +120 -0
  398. package/skills/artifacts-builder/reference/diagramas.md +123 -0
  399. package/skills/artifacts-builder/reference/estilo.md +123 -0
  400. package/skills/artifacts-builder/reference/exportar-pdf.md +56 -0
  401. package/skills/artifacts-builder/reference/graficas.md +22 -0
  402. package/skills/artifacts-builder/scripts/bundle_artifact.py +109 -0
  403. package/skills/artifacts-builder/scripts/validate_artifact.py +345 -0
  404. package/skills/xone-hotswap/SKILL.md +185 -0
  405. package/skills/xone-hotswap/references/comandos.md +706 -0
  406. package/skills/xone-hotswap/references/conexion-y-despliegue.md +226 -0
@@ -0,0 +1,1119 @@
1
+ /**
2
+ * El corredor: coge tareas de la cola, las corre y escribe sus transiciones.
3
+ *
4
+ * **Un solo corredor por máquina**, con cerrojo de pid: dos consolas abiertas serían dos
5
+ * ejecutores sobre la misma cola, y con ellos dos turnos del mismo proyecto a la vez. El
6
+ * segundo proceso sirve el dashboard y NO ejecuta, y lo dice (`corriendoAqui()`): un kanban
7
+ * que se ve igual en dos ventanas pero solo avanza en una tiene que decir en cuál.
8
+ *
9
+ * **Y el cerrojo solo no basta.** Su recogida cuando el dueño parece muerto no se puede
10
+ * hacer atómica con primitivas de sistema de ficheros —«está muerto» es la observación de
11
+ * un instante— y deja un residuo declarado en `agent/tareasEnDisco.ts#recoger` en el que
12
+ * dos procesos pueden creerse dueños. Lo que hace verdad «un solo corredor» son las DOS
13
+ * comprobaciones de aquí, y hacen falta las dos:
14
+ * - `sigoSiendoDueño()` **antes de despachar cada tarea**, no solo al arrancar: es lo que
15
+ * hace que el que perdió el cerrojo se entere ANTES de abrir una consola.
16
+ * - y la ESCRITURA final condicionada a que la tarea siga siendo la nuestra (ver
17
+ * `escribirSiSigueSiendoNuestra`): la comprobación del despacho no cubre el momento de
18
+ * escribir el resultado, que llega minutos después.
19
+ *
20
+ * La política de QUÉ arranca no está aquí: es `core/tareas.ts#siguientesAEjecutar`, pura y
21
+ * con sus tests. Aquí está el lazo, que es lo que no se puede probar sin dobles.
22
+ */
23
+ import { conAutorizadas, conEstado, conVeredicto, siguientesAEjecutar, TOPE_DE_RONDAS_DE_TAREA, } from "../../core/tareas.js";
24
+ import { condicionesDeEntrega, decisionDeEntrega, medidaDeEntrega, } from "../../core/entrega.js";
25
+ import { crearConsolaDeTarea } from "./consolaDeTarea.js";
26
+ import { conAdjuntos } from "../../core/adjuntos.js";
27
+ import { ErrorDelJuezDeTarea } from "../../agent/juezDeTarea.js";
28
+ /**
29
+ * La `revisable` de PRODUCCIÓN, sobre `cambiosDeSesion`.
30
+ *
31
+ * Vive aquí y no en un cierre de `arrancarConsolaWeb` por la razón de siempre en este repo:
32
+ * ahí dentro no se puede llamar, y todos los tests de esa función doblan lo que la rodea —
33
+ * así que esta derivación podía quedarse mal con todo en verde. Medido: una mutación que
34
+ * hacía `escribio: true` a fuego sobrevivió a las 25 mutaciones de la tanda anterior.
35
+ *
36
+ * **`revisable` es «alguien puede mirar esto», y eso vale con las DOS medidas**: la
37
+ * atribución por commit (`via: "git"`) y el respaldo contra la foto de apertura
38
+ * (`desde-apertura`, el único posible en un proyecto donde no se commitea — offline, o la
39
+ * carpeta que abrió una persona). Exigir solo la primera habría dejado sin entregar TODA
40
+ * tarea de un proyecto fuera del workspace, que es donde `commitearTurno` no commitea a
41
+ * propósito. `sin-marca` sigue siendo la única que no se puede revisar.
42
+ *
43
+ * **Lo que sí distingue las dos es `cambiados`**: se quitan los ficheros que nadie ha
44
+ * commiteado, porque no consta de quién son. Para una tarea eso no esconde nada suyo —
45
+ * `commitDeTurno` corre en el `finally` del turno y se ESPERA, así que cuando la puerta de
46
+ * entrega pregunta, lo que la tarea escribió ya está commiteado— y en cambio evita
47
+ * atribuirle lo que estuviera suelto de antes. En el respaldo no hay ninguna fila marcada,
48
+ * así que ahí la lista es exactamente la de siempre: todo lo que cambió desde la apertura,
49
+ * de quien sea.
50
+ *
51
+ * Las tres respuestas posibles, y la del medio es la que sostiene todo:
52
+ * - con ficheros → hay «antes» con que comparar y la sesión cambió algo.
53
+ * - sin ficheros → hay «antes» y la sesión NO cambió nada. Es lo único que permite
54
+ * entregar una tarea de solo lectura, y viene de git y nunca de `autorizadas`, que es la
55
+ * intención del agente y no el hecho.
56
+ * - `sin-marca` → no hay con qué mirarlo, así que **no se afirma nada** sobre si escribió:
57
+ * el campo se queda ausente y la condición del verificador se exige como siempre.
58
+ */
59
+ export function revisionConGit(cambiosDeSesion) {
60
+ return async (raiz, sesion) => {
61
+ const cambios = await cambiosDeSesion(raiz, sesion);
62
+ if (cambios.via !== "git" && cambios.via !== "desde-apertura")
63
+ return { revisable: false };
64
+ /**
65
+ * Las DOS de la misma medida y en la misma expresión, para que `escribio` no pueda
66
+ * decir una cosa y la lista otra. Las rutas ya vienen RELATIVAS de `cambiosDeSesion`
67
+ * (usa `--relative` a propósito, porque el proyecto no tiene por qué ser la raíz del
68
+ * repo), así que por aquí no sale ninguna ruta de la máquina — y eso está MEDIDO contra
69
+ * git de verdad en el test de esta función, no deducido.
70
+ */
71
+ const cambiados = cambios.ficheros.filter((f) => f.sinCommitear !== true).map((f) => f.ruta);
72
+ return { revisable: true, escribio: cambiados.length > 0, cambiados };
73
+ };
74
+ }
75
+ /**
76
+ * Lo que le pasa a una tarea cuya consola se cierra debajo: al parar el proceso, y al
77
+ * encontrarla «en proceso» sin nadie detrás en el arranque siguiente.
78
+ *
79
+ * Es UNA situación —el turno se cortó a mitad y no hay resultado— pero **tiene dos finales
80
+ * distintos, y de eso depende lo que reintentar signifique**. `volcar()` corre en el
81
+ * `finally` del turno, así que un corte a mitad no deja transcript ni entrada en el índice
82
+ * de sesiones; el hilo del checkpointer, en cambio, puede sobrevivir o no:
83
+ * - **Si sobrevive** —hay conversación volcada, así que `sesionAbrible` dice que sí—, el
84
+ * agente recuerda el hilo y reintentar CONTINÚA lo que llevaba hecho. Es el texto de
85
+ * siempre, y sigue siendo verdad en ese caso.
86
+ * - **Si no** —el corte antes de volcar nada—, `olvidarSiNoSePuedeAbrir` acaba de BORRAR
87
+ * el checkpoint: un id que no lleva a ninguna parte son además decenas de megas que
88
+ * nadie puede alcanzar. Ahí reintentar arranca un hilo nuevo con el encargo entero
89
+ * (`AVISO_SIN_HILO_REANUDABLE`), y decir que el agente lo recuerda miente dos veces:
90
+ * sobre lo que pasó y sobre lo que va a pasar.
91
+ *
92
+ * **De dónde sale que sean dos y no uno.** El texto único se escribió cuando el hilo SÍ
93
+ * quedaba escrito siempre, y un commit posterior introdujo `olvidarSiNoSePuedeAbrir`, que
94
+ * lo borra en ese mismo caso. Los dos cambios eran correctos contra su propio encargo; la
95
+ * contradicción solo se veía leyéndolos juntos. Por eso el motivo se compone DESPUÉS de
96
+ * saber si el hilo se olvidó, y no antes: quien lo escribe es el único que lo sabe.
97
+ */
98
+ export const MOTIVO_CORTADA_POR_CIERRE = "la consola se cerró a mitad del turno: no quedó respuesta guardada, aunque el agente " +
99
+ "recuerda el hilo. Reintenta cuando quieras.";
100
+ /** El mismo corte, con el hilo ya olvidado. Ver arriba. */
101
+ export const MOTIVO_CORTADA_SIN_HILO = "la consola se cerró a mitad del turno: no quedó respuesta guardada y tampoco nada que " +
102
+ "reabrir, así que el hilo del agente se olvidó. Reintentar empieza de cero, con el " +
103
+ "encargo entero.";
104
+ /** Cuál de los dos, según lo que la regla de `sesion` acabara de decidir. */
105
+ export const motivoDeCorteAMitad = (hiloOlvidado) => hiloOlvidado ? MOTIVO_CORTADA_SIN_HILO : MOTIVO_CORTADA_POR_CIERRE;
106
+ /**
107
+ * Cuando llega un feedback pero el hilo anterior YA NO se puede reanudar —`Tarea.sesion`
108
+ * está ausente porque la regla de `sesion` ya lo limpió al no haber nada abrible—, el turno
109
+ * arranca un hilo NUEVO sin memoria de nada. Mandarle solo el feedback a ese hilo en blanco
110
+ * sería una frase sin sujeto: «sí, con histórico» no dice nada sin el encargo delante. Así
111
+ * que se dice la verdad —esto no continúa nada— y se manda el encargo entero al lado. Es
112
+ * la mitad de «no se finge que continúa» que le toca a esta capa: la otra mitad, que
113
+ * `sesion` sobreviva siempre que haya algo que abrir, ya está en la regla de `sesion`.
114
+ */
115
+ export const AVISO_SIN_HILO_REANUDABLE = "el hilo anterior de esta tarea ya no se pudo reanudar (no quedó nada que reabrir): esto " +
116
+ "es un turno nuevo, sin memoria de lo anterior. El encargo original y el feedback del " +
117
+ "desarrollador, juntos:";
118
+ /**
119
+ * La petición que se le manda al turno cuando la tarea trae un feedback pendiente.
120
+ *
121
+ * **Reanudando, el feedback SOLO** — el mismo patrón que los hallazgos del verificador
122
+ * (`agent/turnoReal.ts#conVerificacion`): entra como un mensaje de USUARIO más en el hilo
123
+ * que ya sabe el encargo, lo que se intentó y por qué se aparcó. Repetir el encargo ahí
124
+ * sería ruido: el modelo ya lo tiene.
125
+ *
126
+ * **Sin hilo que reanudar, el encargo entero, con el aviso delante.** Ver
127
+ * `AVISO_SIN_HILO_REANUDABLE`.
128
+ */
129
+ export const peticionDeFeedback = (encargo, texto, reanudando) => reanudando ? texto : `${AVISO_SIN_HILO_REANUDABLE}\n\nEncargo original:\n${encargo}\n\nFeedback:\n${texto}`;
130
+ /**
131
+ * Lo que se espera como mucho a que un turno cortado devuelva, al parar el proceso.
132
+ *
133
+ * Generoso pero FINITO: un Ctrl-C que se cuelga para siempre es lo peor que puede hacer este
134
+ * camino, y el repo ya trata esta clase con tope (el `TOPE_MS` de Codex, los `TOPES_MS` de
135
+ * los dispositivos). Al agotarse no se miente diciendo que se paró: la tarea se queda «en
136
+ * proceso» y la reconciliación del siguiente arranque la aparca, que es justo para lo que
137
+ * existe.
138
+ */
139
+ export const TOPE_DE_PARADA_MS = 30_000;
140
+ /**
141
+ * Lo que se espera, como MUCHO, a que el juez de QA conteste.
142
+ *
143
+ * Es la regla de siempre en este repo —«cada proceso lleva tope», y «un cuelgue se dice
144
+ * como *no respondió*, nunca se queda el panel en *consultando…*»— aplicada a una llamada
145
+ * de modelo: los SDK traen plazos por omisión del orden de diez minutos, y durante todo ese
146
+ * rato la tarea diría «en proceso» ocupando el hueco y el proyecto sin que nadie pueda
147
+ * saber por qué. Dos minutos es de sobra para un veredicto de dos frases sobre una lista de
148
+ * nombres.
149
+ *
150
+ * Al agotarse NO se entrega: la consulta se abandona (la llamada sigue viva en segundo
151
+ * plano, no hay con qué abortarla desde aquí) y la tarea queda esperando feedback con el
152
+ * motivo — que es lo mismo que hace `parar()` cuando su plazo se agota: no mentir.
153
+ */
154
+ export const TOPE_DEL_JUEZ_MS = 120_000;
155
+ /** El mismo motivo, diciendo qué proceso la tenía. Ver `motivoDeCorteAMitad`. */
156
+ const motivoDeReconciliacion = (pid, hiloOlvidado) => {
157
+ const motivo = motivoDeCorteAMitad(hiloOlvidado);
158
+ return pid === undefined ? motivo : `${motivo} (era el pid ${pid})`;
159
+ };
160
+ /**
161
+ * Del `ConsolaDeProyecto` que abre el vestíbulo al `ConsolaParaTarea` que el corredor pide.
162
+ *
163
+ * Aquí se junta todo: la segunda puerta (que no mueve el cable), la consola que APARCA en
164
+ * vez de contestar por nadie, y el ejecutor de siempre — el mismo que corre un turno de una
165
+ * persona, con las mismas barreras.
166
+ *
167
+ * **Vive aquí y no en el cierre de `arranque.ts`** —donde el plan la puso— por una razón
168
+ * medible: desde ahí no se puede llamar, y la propiedad que sostiene (que la sesión de la
169
+ * tarea se GUARDE) es justo la que se rompe en silencio. El `MEDIDO:` de `vestibulo.test.ts`
170
+ * no la vigila: el agujero está en qué piel se le pasa, así que ese test se queda verde
171
+ * tanto con el arreglo como sin él.
172
+ *
173
+ * **La PIEL es la que hace que el turno se guarde**, y es el único motivo de que esta
174
+ * función sea algo más que tres campos reenviados. `volcar()` (`vestibulo.ts`) lee los actos
175
+ * de la consola de PROYECTO, así que un turno corrido con una `Consola` ajena no vuelca
176
+ * nada: la conversación se pinta en un objeto que nadie lee y la tarea acaba con su sesión
177
+ * vacía. Pasándole `consola.consola.consola.piel` —la misma que usa una persona— el turno
178
+ * escribe actos de asistente, de razonamiento y las tarjetas de artefacto en el transcript
179
+ * de SU sesión, que es lo que hace que atender la tarea sea abrir su conversación.
180
+ */
181
+ export function consolaParaTarea(consola) {
182
+ const dentro = consola.consola.consola;
183
+ return {
184
+ raiz: consola.raiz,
185
+ idDeHilo: consola.idDeHilo,
186
+ correrTarea: async (encargo, aparcar, autorizado) => {
187
+ // Una consola de tarea por TURNO: su «se aparca una vez» vale por turno, y reusarla
188
+ // para un segundo turno lo dejaría mudo.
189
+ const deTarea = {
190
+ ...crearConsolaDeTarea({
191
+ aparcar,
192
+ // Lo que la tarea autorice sin aprobación sube al corredor, que lo guarda con el
193
+ // estado final. Se reenvía o no está, como la piel: `autorizado: undefined` haría
194
+ // que un `"autorizado" in opciones` dijera que sí.
195
+ ...(autorizado === undefined ? {} : { autorizado }),
196
+ // Lo que escriba el turno va al transcript de SU sesión.
197
+ escribir: (texto) => dentro.escribir(texto),
198
+ ...(dentro.piel === undefined ? {} : { piel: dentro.piel.bind(dentro) }),
199
+ catalogoModelos: dentro.catalogoModelos,
200
+ guardarModeloGlobal: dentro.guardarModeloGlobal,
201
+ }),
202
+ /**
203
+ * **El tope de rondas de una TAREA, que no es el de la persona.**
204
+ *
205
+ * Se cablea aquí y no dentro de `crearConsolaDeTarea` porque este es el punto de
206
+ * MONTAJE —el mismo motivo por el que esta función vive en este fichero y no en el
207
+ * cierre de `arranque.ts`—: quién está detrás lo sabe quien junta las piezas.
208
+ *
209
+ * El porqué del número está en `TOPE_DE_RONDAS_DE_TAREA`. El porqué de que haga
210
+ * falta está medido: con las escrituras aprobándose solas, cada tanda gastaba una de
211
+ * las cinco rondas de la persona, y un turno acabó con cuatro ficheros escritos, una
212
+ * escritura abandonada y el verificador sin correr ni una vez.
213
+ */
214
+ topeDeAprobaciones: TOPE_DE_RONDAS_DE_TAREA,
215
+ };
216
+ // Se DEVUELVE lo que el turno informe: es con lo que el corredor mide si el trabajo
217
+ // se puede dar por bueno. Sin este `return`, la puerta de la entrega no tendría datos
218
+ // y toda tarea acabaría esperando feedback — la dirección segura, pero inútil.
219
+ return await consola.ejecutarTurno(encargo, consola.estadoDeSesion, deTarea);
220
+ },
221
+ cerrar: () => consola.cerrar(),
222
+ esperarMarca: () => consola.esperarMarca(),
223
+ // A la consola WEB y por `mirar`, nunca por `conectar`: ver `ConsolaParaTarea.mirar`.
224
+ mirar: (enviar) => consola.consola.mirar(enviar),
225
+ dejarDeMirar: (enviar) => consola.consola.dejarDeMirar(enviar),
226
+ };
227
+ }
228
+ export function crearCorredorDeTareas(opciones) {
229
+ const pid = opciones.pid ?? process.pid;
230
+ const informar = opciones.informar ?? (() => { });
231
+ let miCerrojo = false;
232
+ /** Ver `Corredor.ejecutaOtroProceso`. Nace sin afirmar nada: todavía no se ha mirado. */
233
+ let cerrojoAjeno;
234
+ let parando = false;
235
+ /**
236
+ * Las que este proceso no vuelve a coger, por DOS motivos distintos que comparten el
237
+ * mismo freno.
238
+ *
239
+ * **El original: no pudo escribir su estado.** Es el freno de un lazo que no tiene
240
+ * temporizador: `revisar` se dispara al terminar cada tarea, así que una que se queda en
241
+ * `nuevo` porque el índice no se puede escribir (disco lleno, permisos) se elegiría otra
242
+ * vez, y otra, sin decir nada — medido con la transición que faltaba: no es un error que
243
+ * se lee, es una CPU al 100%. Solo se renuncia cuando se ha COMPROBADO que sigue en
244
+ * `nuevo` tras intentar moverla; que ya no esté o que la tenga otro no es renunciar, es
245
+ * que no era nuestra.
246
+ *
247
+ * **El de Task 14: se pidió cortarla (`cortarPedido`) antes de que llegara a marcar «en
248
+ * proceso».** Ese corte también deja la tarea en `nuevo` —nunca llegó a escribir nada—, y
249
+ * es EL MISMO lazo caliente: `entrada.trabajo` se asienta, su `.finally()` llama a
250
+ * `revisar()` en el acto, y sin esta entrada el turno recién cortado arrancaría de nuevo
251
+ * un instante después. Medido: sin ella, `Corredor.cortar(id)` sobre una tarea que aún no
252
+ * había abierto consola SÍ la cortaba… y el redespacho inmediato la corría entera, con su
253
+ * encargo mandado y todo — el fallo exacto que esta tarea existe para cerrar, reabierto
254
+ * por el propio arreglo.
255
+ *
256
+ * Los dos casos se olvidan igual: al reiniciar el proceso, que es cuando el disco pudo
257
+ * cambiar — y para el segundo caso además da igual, porque una tarea CORTADA se borra
258
+ * segundos después y su id no vuelve a existir.
259
+ */
260
+ const renunciadas = new Set();
261
+ const enVuelo = new Map();
262
+ /**
263
+ * Escribe UNA tarea, releyendo el índice y **solo si en disco sigue siendo la nuestra**.
264
+ *
265
+ * Releer hace falta porque entre medias la cola pudo cambiar por el cable (otra pestaña
266
+ * renombra el encargo, descarta la tarea) o por el otro corredor. Y la CONDICIÓN es lo
267
+ * que evita el daño: si la tarea ya no está —descartada—, escribirla la resucitaría; si
268
+ * está «en proceso» con otro pid, el otro corredor la está corriendo de verdad y poner
269
+ * «terminada» encima la daría por buena sin que nadie la haya hecho.
270
+ *
271
+ * Devuelve si escribió, porque quien despacha necesita saberlo: una marca de «en proceso»
272
+ * que no se pudo escribir significa que esa tarea ya no es nuestra y no hay que correrla.
273
+ */
274
+ const escribirSiSigueSiendoNuestra = (id, siguiente) => {
275
+ const lista = opciones.disco.listar();
276
+ const actual = lista.find((t) => t.id === id);
277
+ if (actual === undefined)
278
+ return false;
279
+ const cambiada = siguiente(actual);
280
+ if (cambiada === undefined)
281
+ return false;
282
+ const nueva = lista.map((t) => (t.id === id ? cambiada : t));
283
+ opciones.disco.guardar(nueva);
284
+ opciones.alCambiar?.(nueva);
285
+ return true;
286
+ };
287
+ /**
288
+ * El código de un error, nunca su mensaje.
289
+ *
290
+ * El mensaje de Node lleva la ruta absoluta («ENOENT: … open '/Users/…/app.xne'») y el
291
+ * motivo se pinta en el kanban, que viaja por el cable — que puede ir por un túnel. Es la
292
+ * misma regla y el mismo apaño que `codigoDe` en `arranque.ts`.
293
+ */
294
+ const codigoDe = (error) => {
295
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") {
296
+ return error.code;
297
+ }
298
+ return error instanceof Error ? error.name : "error";
299
+ };
300
+ /**
301
+ * El mensaje si lo escribimos nosotros, y el código si lo escribió el sistema.
302
+ *
303
+ * Un error de Node trae `code` y su mensaje trae la ruta absoluta; uno escrito a mano en
304
+ * este repo no trae `code` y su mensaje es justo lo que hay que leer. Es la única forma de
305
+ * quedarse con «falta su .xonecode/config.json» sin quedarse también con
306
+ * «…, open '/Users/quien-sea/…'».
307
+ */
308
+ const sinRutas = (error) => typeof error === "object" && error !== null && "code" in error ? codigoDe(error) : unaLinea(error);
309
+ /**
310
+ * La tarea con su `sesion`, **si y solo si hay algo que una persona pueda abrir**; y si no
311
+ * la hay, se OLVIDA el hilo del agente.
312
+ *
313
+ * Con transcript volcado, la entrada está en el índice del proyecto y la conversación se
314
+ * lee desde la barra lateral: se conserva. Sin transcript —el corte a mitad de turno que
315
+ * se midió: checkpoint sí, transcript no— ese id no nombra nada abrible, así que se olvida
316
+ * el hilo y se limpia `sesion`. Un campo que apunta a algo que no se puede abrir es la
317
+ * misma mentira que un control sin dato detrás.
318
+ *
319
+ * Y olvidarlo no es cosmético: un checkpoint es la lista de mensajes ENTERA —contenido de
320
+ * ficheros incluido— y CRECE (medido en CLAUDE.md: cinco turnos, 370 checkpoints, 30 MB).
321
+ * Dejarlo vivo e inalcanzable desde la interfaz es exactamente lo que `borrarSesion` evita
322
+ * al borrar una conversación.
323
+ */
324
+ const olvidarSiNoSePuedeAbrir = async (raiz, sesion) => {
325
+ if (sesion === undefined || opciones.sesionAbrible === undefined)
326
+ return false;
327
+ try {
328
+ if (opciones.sesionAbrible(raiz, sesion))
329
+ return false;
330
+ }
331
+ catch {
332
+ // Preguntarlo puede fallar (en producción lee el índice del proyecto). Entonces no se
333
+ // sabe, y no se borra nada: la misma dirección que el puerto ausente.
334
+ return false;
335
+ }
336
+ // Primero olvidar y después escribir: si la escritura falla, el hilo que se ha olvidado
337
+ // era inalcanzable de todas formas. Un fallo al olvidar no puede impedir la limpieza del
338
+ // campo — el mismo trato que `olvidarHilo` le da a `borrarSesion`.
339
+ await opciones.olvidarHilo?.(raiz, sesion).catch(() => undefined);
340
+ return true;
341
+ };
342
+ /** La tarea sin su `sesion`. Ver la regla de arriba. */
343
+ const sinSesion = (tarea) => {
344
+ const limpia = { ...tarea };
345
+ delete limpia.sesion;
346
+ return limpia;
347
+ };
348
+ /**
349
+ * Aparca una tarea pasando por la regla de `sesion`, releyendo del disco el id con el que
350
+ * quedó marcada.
351
+ *
352
+ * Existe para que el camino de ERROR pase por las MISMAS dos cosas que el bueno: una regla
353
+ * que se cumple salvo cuando algo falla no es una regla. Y relee el id en vez de recibirlo
354
+ * porque desde el último recurso del lazo no se sabe con qué hilo se marcó.
355
+ */
356
+ const aparcarMirandoLaSesion = async (tarea, motivo,
357
+ /** Lo que el turno autorizó, si llegó a correr. Ver `EnVuelo.autorizadas`. */
358
+ autorizadas,
359
+ /** Lo que dijo el juez, si se llegó a preguntar. Ver `EnVuelo.veredicto`. */
360
+ veredicto) => {
361
+ const actual = opciones.disco.listar().find((t) => t.id === tarea.id);
362
+ const limpiar = await olvidarSiNoSePuedeAbrir(tarea.proyecto.raiz, actual?.sesion);
363
+ // Las TRES reglas, aquí también: por este camino se llega cuando `correr` se rompió por
364
+ // su cuenta, y era el único de los cuatro que perdía el registro de lo autorizado.
365
+ const conLoQueQuede = (t) => conVeredicto(conAutorizadas(limpiar ? sinSesion(t) : t, autorizadas), veredicto);
366
+ aparcar(tarea.id, motivo, conLoQueQuede);
367
+ };
368
+ /**
369
+ * Que el juez no se pueda usar es fallo del ENTORNO, y el motivo tiene que decirlo.
370
+ *
371
+ * `ErrorDelJuezDeTarea` ya se nombra a sí mismo y ya trae, cuando lo hay, el mensaje
372
+ * accionable —el de CONSTRUIR el modelo: «falta la credencial para nvidia
373
+ * (NVIDIA_API_KEY); usa /provider nvidia»—, así que se usa tal cual y no se le pone otro
374
+ * prefacio encima. Cualquier otra cosa que lance el puerto se nombra desde aquí: un
375
+ * motivo que no dijera quién falló mandaría a buscar el problema en el trabajo del
376
+ * agente.
377
+ */
378
+ const motivoDelJuez = (error) => error instanceof ErrorDelJuezDeTarea
379
+ ? unaLinea(error)
380
+ : `no se pudo consultar al juez de QA (${sinRutas(error)})`;
381
+ /**
382
+ * **LA PUERTA DE LA ENTREGA**: qué impide dar la tarea por terminada, o `undefined` si
383
+ * nada lo impide.
384
+ *
385
+ * Las condiciones las comprueba el CÓDIGO y el juez opina, y hacen falta las dos: es la
386
+ * regla que este repo ya tenía escrita para la subida autónoma
387
+ * (`core/cloudstudio.ts#PoliticaDeAprobacion`), y el motivo es el mismo por el que los
388
+ * avisos de honestidad son código y no prompt — a un modelo se le puede pedir que avise y
389
+ * a veces no avisa. Quién DECIDE es `core/entrega.ts`, que es puro; aquí solo se mide.
390
+ *
391
+ * **Al juez se le pregunta DESPUÉS de medir, y solo si las condiciones pasan.** Cada
392
+ * consulta es una llamada al modelo del papel `afilado`, el más caro del reparto, y
393
+ * gastarla para tapar un hecho que ya se sabe es gastarla para nada. `decisionDeEntrega`
394
+ * vuelve a comprobar las condiciones de todas formas: la garantía no puede depender de
395
+ * que quien llama lo haya hecho en este orden.
396
+ */
397
+ const puertaDeEntrega = async (tarea, sesion, entrada, resultado,
398
+ /** Espera a que la ref de la sesión esté escrita, si quien abrió sabe hacerlo. */
399
+ esperarMarca) => {
400
+ /**
401
+ * Antes de preguntarle a git, esperar a que git haya acabado de escribir.
402
+ *
403
+ * `volcar()` (`vestibulo.ts`) apunta la ref de la sesión sin aguardarla, y medido con
404
+ * git de verdad la ref todavía no estaba cuando esto medía: `cambiosDeSesion` devolvía
405
+ * `sin-marca` y **toda** tarea se aparcaba diciendo que nadie podía revisarla, en
406
+ * proyectos donde sí se podía. La espera está AQUÍ y no en `cerrar()` porque quien la
407
+ * necesita es esto: cerrar un proyecto estando sentado delante no puede pasar a
408
+ * aguardar un `write-tree` del árbol entero.
409
+ */
410
+ await esperarMarca?.().catch(() => undefined);
411
+ let revision = { revisable: false };
412
+ try {
413
+ revision = await opciones.revisable(tarea.proyecto.raiz, sesion);
414
+ }
415
+ catch {
416
+ // Ni preguntarlo se pudo (en producción es git). Entonces no se sabe, y no se
417
+ // entrega: la única dirección posible aquí, y la misma que toma el resto del fichero.
418
+ revision = { revisable: false };
419
+ }
420
+ /**
421
+ * Las rutas autorizadas se normalizan UNA vez y sirven para las dos cosas: la cuenta que
422
+ * mide la condición de «nada aterrizó» y la lista que ve el juez. Normalizadas por el
423
+ * MISMO sitio que las guarda en el índice, así que son exactamente las que verá la
424
+ * persona — relativas, sin repetidos y sin la barra del backend virtual.
425
+ */
426
+ const autorizadas = conAutorizadas(tarea, entrada.autorizadas ?? []).autorizadas ?? [];
427
+ const medida = medidaDeEntrega(resultado, revision, autorizadas.length);
428
+ const condiciones = condicionesDeEntrega(medida);
429
+ if (!condiciones.entregable)
430
+ return condiciones;
431
+ /**
432
+ * Y si el proceso se está yendo, NO se pregunta — y se dice la verdad de este caso, que
433
+ * no es ninguna de las otras.
434
+ *
435
+ * Sin esta guarda, un Ctrl-C durante la consulta dejaba a la tarea colgada del plazo de
436
+ * `parar()`; al agotarse se queda «en proceso» y la reconciliación del arranque
437
+ * siguiente la aparca con `MOTIVO_CORTADA_POR_CIERRE`, que aquí sería FALSO: dice que
438
+ * no quedó respuesta guardada, y el turno acabó y `volcar()` ya escribió su transcript.
439
+ * Lo único que se interrumpió fue el juicio, y eso es lo que hay que poder leer.
440
+ */
441
+ if (parando) {
442
+ return {
443
+ entregable: false,
444
+ motivo: "la consola se cerró antes de evaluar la entrega: el turno acabó y su conversación " +
445
+ "está guardada; reintenta para que el juez la mire",
446
+ };
447
+ }
448
+ let veredicto;
449
+ let fallo;
450
+ let reventó = false;
451
+ /**
452
+ * La consulta, con PLAZO. Ver `TOPE_DEL_JUEZ_MS`: una llamada de modelo colgada dejaría
453
+ * la tarea diciendo «en proceso» durante el plazo del SDK, con su hueco y su proyecto
454
+ * ocupados y sin que nadie pueda saber por qué.
455
+ */
456
+ const consulta = opciones.juez
457
+ .juzgar({
458
+ encargo: tarea.encargo,
459
+ raiz: tarea.proyecto.raiz,
460
+ autorizadas,
461
+ verificador: medida.verificador,
462
+ ...(medida.hallazgos === undefined ? {} : { hallazgos: medida.hallazgos }),
463
+ /**
464
+ * Y el otro lado del REPARTO, más lo que dice git. Las dos cosas son datos que ya
465
+ * estaban medidos y se tiraban, y sin ellas el prompt del juez no puede decir la
466
+ * verdad sobre lo que le está dando: `hallazgos` es una lista ya filtrada (sin el
467
+ * número no se sabe que lo está) y `autorizadas` es la intención del agente (sin
468
+ * `escribio` no hay ningún hecho sobre el disco). Medido en la primera ejecución
469
+ * real: el juez dictó rojo acusando al turno de modificar ficheros de los que solo
470
+ * hablaban los hallazgos, y diciendo que no podía comprobar un fichero que estaba
471
+ * en `autorizadas`.
472
+ */
473
+ ...(medida.preexistentes === undefined ? {} : { preexistentes: medida.preexistentes }),
474
+ ...(medida.cambiados === undefined ? {} : { cambiados: medida.cambiados }),
475
+ })
476
+ .then((v) => void (veredicto = v))
477
+ .catch((error) => {
478
+ reventó = true;
479
+ fallo = error;
480
+ });
481
+ if (!(await conPlazo(consulta, opciones.esperaDelJuez ?? TOPE_DEL_JUEZ_MS))) {
482
+ return {
483
+ entregable: false,
484
+ motivo: `el juez de QA no contestó en ${Math.round((opciones.esperaDelJuez ?? TOPE_DEL_JUEZ_MS) / 1000)} s`,
485
+ };
486
+ }
487
+ if (reventó)
488
+ return { entregable: false, motivo: motivoDelJuez(fallo) };
489
+ entrada.veredicto = veredicto;
490
+ return decisionDeEntrega(medida, veredicto);
491
+ };
492
+ /**
493
+ * Comprueba que la tarea salió de `nuevo`, y si no, renuncia a ella. Ver `renunciadas`.
494
+ */
495
+ const renunciarSiSigueNueva = (id, error) => {
496
+ // Renunciar dos veces es renunciar una: el camino de la marca que no se pudo escribir
497
+ // pasa por aquí con el error de verdad y otra vez, más abajo, sin él — y ese segundo
498
+ // aviso salía con «error» en vez del `EACCES` que es justo el dato que sirve. Dos avisos
499
+ // para un hecho, y el segundo peor que el primero, es cómo se enseña a no leerlos.
500
+ if (renunciadas.has(id))
501
+ return;
502
+ if (opciones.disco.listar().find((t) => t.id === id)?.estado !== "nuevo")
503
+ return;
504
+ renunciadas.add(id);
505
+ // Esto SÍ es del proceso y no de la tarea: su cola no se puede escribir, así que ni el
506
+ // motivo llegaría al kanban. Es el mismo criterio que el aviso del cerrojo.
507
+ informar(`no se pudo escribir el estado de una tarea (${codigoDe(error)}); esta consola no la vuelve a coger`);
508
+ };
509
+ /**
510
+ * Aparca la tarea, y si no se puede aparcar no se insiste.
511
+ *
512
+ * `conEstado` LANZA ante una transición imposible, y aquí eso solo puede pasar si la
513
+ * tarea cambió de estado por debajo — o sea, justo el caso en el que no hay que escribir.
514
+ */
515
+ const aparcar = (id, motivo, antes = (t) => t) => {
516
+ try {
517
+ escribirSiSigueSiendoNuestra(id, (actual) => actual.estado === "en-proceso" && actual.pid === pid
518
+ ? conEstado(antes(actual), "requiere-atencion", motivo)
519
+ : undefined);
520
+ }
521
+ catch (error) {
522
+ // Ver arriba: si no se puede, es porque ya no es nuestra… o porque el índice no se
523
+ // puede escribir, y eso sí hay que frenarlo.
524
+ renunciarSiSigueNueva(id, error);
525
+ }
526
+ };
527
+ /**
528
+ * Dónde están los adjuntos de esta tarea, **solo si tiene alguno**.
529
+ *
530
+ * Las dos mitades importan. Se monta solo con adjuntos porque una raíz vacía sería mandar
531
+ * al agente a mirar un sitio donde no hay nada — el «control sin dato detrás» de siempre,
532
+ * aquí en forma de carpeta. Y se le pregunta al PUERTO en vez de componer la ruta a mano:
533
+ * él es quien comprueba el id y quien recomprueba el camino real (un enlace simbólico en
534
+ * la cola daría lectura fuera de ella), y su `undefined` significa «no se puede montar» —
535
+ * que aquí se respeta tal cual, sin ruta de reserva.
536
+ */
537
+ const carpetaDeAdjuntos = (tarea) => tarea.adjuntos.length === 0 ? undefined : opciones.disco.carpetaDeAdjuntos(tarea.id);
538
+ const correr = async (tarea, entrada) => {
539
+ let consola;
540
+ try {
541
+ // `tarea.sesion`, si la hay, es lo que hace que reanudar siga la MISMA conversación:
542
+ // ver el comentario de `abrirParaTarea` más arriba.
543
+ // Y el id de la TAREA, que es lo que marca su sesión en el índice del proyecto: sin
544
+ // él la conversación de una tarea entra en la barra como una más —y con el título
545
+ // vacío, porque el título sale del primer acto de `usuario` y una tarea no manda
546
+ // ninguno—. Ver `EntradaIndice.tarea` para por qué no se deduce cruzando con la cola.
547
+ consola = await opciones.abrirParaTarea(tarea.proyecto.raiz, tarea.sesion, carpetaDeAdjuntos(tarea), tarea.id);
548
+ }
549
+ catch (error) {
550
+ /**
551
+ * El proyecto ya no está donde la tarea dice. Se aparca: la dirección de fallo aquí es
552
+ * no ejecutar, nunca ejecutar contra otra carpeta.
553
+ *
554
+ * Aquí el mensaje SÍ se usa, y es la única excepción — pero solo el de la guarda de
555
+ * `abrirParaTarea` (`vestibulo.ts`), que está escrito para subir al registro de la
556
+ * tarea y no lleva ninguna ruta a propósito: es el que dice qué hacer (falta el
557
+ * `config.json`). Lo que viene DETRÁS de esa guarda no es nuestro —
558
+ * `dependenciasDeProyecto`, `crearEjecutor`, la foto de git— y puede lanzar un error de
559
+ * Node con el home dentro. Se distinguen por lo mismo que en todo el repo: un error del
560
+ * sistema trae `code`, y uno escrito a mano no. Ver `codigoDe`.
561
+ */
562
+ try {
563
+ escribirSiSigueSiendoNuestra(tarea.id, (actual) => actual.estado === "nuevo"
564
+ ? conEstado(actual, "requiere-atencion", `no se pudo abrir el proyecto: ${sinRutas(error)}`)
565
+ : undefined);
566
+ }
567
+ finally {
568
+ // Si sigue en `nuevo`, este proceso no la vuelve a coger: `revisar` se dispara al
569
+ // acabar cada tarea, así que reintentarla sería un lazo caliente. Ver `renunciadas`.
570
+ renunciarSiSigueNueva(tarea.id, error);
571
+ }
572
+ return;
573
+ }
574
+ let cerrada = false;
575
+ const cortar = async () => {
576
+ if (cerrada)
577
+ return;
578
+ cerrada = true;
579
+ await consola.cerrar().catch(() => { });
580
+ };
581
+ // Desde aquí, `parar()` sabe cómo cortar ESTE turno. Antes de esta línea la consola no
582
+ // existía todavía y no había nada que cortar.
583
+ entrada.cortar = cortar;
584
+ /**
585
+ * Y desde aquí se puede mirar. Lo que ya estaba pedido se engancha ahora, con una
586
+ * REEMISIÓN del transcript por delante: quien pulsó «ver» durante la apertura recibió
587
+ * una lista vacía, y sin esto se quedaría esperando el primer acto NUEVO — perdiéndose
588
+ * todo lo que el turno hubiera pintado entre medias.
589
+ */
590
+ entrada.consola = consola;
591
+ for (const miron of entrada.mirones) {
592
+ const actos = consola.mirar?.(miron);
593
+ if (actos !== undefined)
594
+ miron({ clase: "reemision", actos: [...actos] });
595
+ }
596
+ // Y si el corte llegó mientras se abría, no se arranca nada: marcar «en proceso» ahora
597
+ // dejaría una tarea corriendo justo cuando el proceso se está yendo — o, con
598
+ // `cortarPedido`, justo cuando alguien la acaba de descartar. Los dos casos comparten
599
+ // esta puerta a propósito: ninguno de los dos puede dejar arrancar el turno.
600
+ if (parando || entrada.cortarPedido) {
601
+ await cortar();
602
+ // Igual que `renunciadas` un poco más abajo, y por el mismo mecanismo: si esta tarea
603
+ // se queda «nuevo» en disco, `revisar()` la volvería a coger en el MISMO tick —su
604
+ // `.finally()` llama a `revisar()` en cuanto este `correr()` termina— y el turno que
605
+ // se acaba de cortar arrancaría un instante después. Medido: sin esta línea, cortar
606
+ // una tarea que todavía no había llegado a marcar «en proceso» la cortaba de
607
+ // verdad… y el REDESPACHO inmediato la volvía a correr entera, con su encargo y todo.
608
+ // Solo para `cortarPedido`: con `parando`, `revisar()` ya se frena sola en su primera
609
+ // línea (`if (!miCerrojo || parando) return;`), así que ahí no hace falta.
610
+ if (entrada.cortarPedido)
611
+ renunciadas.add(tarea.id);
612
+ return;
613
+ }
614
+ const hilo = consola.idDeHilo;
615
+ /**
616
+ * ¿Esto CONTINÚA una conversación, o abre una en blanco? `tarea.sesion` es la única
617
+ * fuente: si estaba puesta, es lo que se le pasó a `abrirParaTarea` para que reabriera
618
+ * justo esa —y con ella el checkpointer trae la memoria entera—; si no, `hilo` es una
619
+ * sesión nueva y el modelo no sabe nada de lo anterior. Ver `peticionDeFeedback`.
620
+ */
621
+ const reanudando = tarea.sesion !== undefined;
622
+ let motivo;
623
+ /**
624
+ * Lo que el turno informó de sí mismo, o `undefined` si no informó — que es «no se
625
+ * sabe» y NO «todo bien» (`medidaDeEntrega`). De aquí sale la medida de la entrega.
626
+ */
627
+ let resultado;
628
+ /** ¿Devolvió el turno por su cuenta? Distingue «acabó» de «se lo cortamos». */
629
+ let terminoLimpio = false;
630
+ /**
631
+ * Si el final es «el turno se cortó a mitad», que tiene DOS textos según sobreviva o no
632
+ * el hilo del agente (ver `motivoDeCorteAMitad`). Se decide aquí y se REDACTA abajo,
633
+ * cuando ya se sabe: al revés, la tarjeta diría que el agente recuerda el hilo justo en
634
+ * la rama donde el código acaba de borrarlo.
635
+ */
636
+ let cortadaAMitad = false;
637
+ let marcada = false;
638
+ /**
639
+ * Lo que de verdad se le manda al turno: el encargo la primera vez, o el feedback
640
+ * pendiente si lo hay (`peticionDeFeedback`). Se decide DENTRO de la escritura CAS de
641
+ * abajo, sobre la tarea RELEÍDA del disco y no sobre el parámetro `tarea`: solo esa
642
+ * lectura sabe con qué feedback exacto se despacha esta pasada — el mismo motivo por
643
+ * el que `escribirSiSigueSiendoNuestra` relee en vez de fiarse de lo que se le pasó.
644
+ */
645
+ let peticion = tarea.encargo;
646
+ try {
647
+ /**
648
+ * La marca de «en proceso», y es una PUERTA: si no se puede escribir, esta tarea ya no
649
+ * es nuestra —la descartaron, o el otro corredor se la llevó— y no se corre. Sin esa
650
+ * puerta el turno arrancaría igual sobre una tarea que en el kanban ya no existe.
651
+ *
652
+ * El id del hilo ES la sesión: es lo que hace que atender la tarea sea abrir su
653
+ * conversación. Y aquí también se CONSUME el feedback pendiente, si lo hay, en la
654
+ * MISMA escritura que marca «en proceso»: si se consumiera aparte, un fallo entre
655
+ * medias dejaría un feedback marcado sin haberse mandado, o mandado sin marcar —y la
656
+ * vuelta siguiente lo repetiría.
657
+ */
658
+ marcada = escribirSiSigueSiendoNuestra(tarea.id, (actual) => {
659
+ if (actual.estado !== "nuevo")
660
+ return undefined;
661
+ const pendiente = actual.feedback?.find((f) => !f.consumido);
662
+ const conPendienteConsumido = pendiente === undefined
663
+ ? actual
664
+ : { ...actual, feedback: actual.feedback.map((f) => (f === pendiente ? { ...f, consumido: true } : f)) };
665
+ if (pendiente !== undefined)
666
+ peticion = peticionDeFeedback(actual.encargo, pendiente.texto, reanudando);
667
+ // El inventario de los adjuntos va DETRÁS de lo que se manda esta pasada, sea el
668
+ // encargo o el feedback: montar `/adjuntos/` no basta, porque es una raíz virtual
669
+ // que ninguna instrucción del agente nombra. Ver `core/adjuntos.ts#conAdjuntos`.
670
+ peticion = conAdjuntos(peticion, actual.adjuntos);
671
+ return conEstado({ ...conPendienteConsumido, sesion: hilo }, "en-proceso", undefined, { pid });
672
+ });
673
+ if (marcada) {
674
+ // La lista nace AQUÍ, cuando ya se sabe que el turno va a correr: hasta este punto
675
+ // «no consta» es la verdad. Se acumula aunque el turno reviente —`preguntar` corta
676
+ // lanzando— porque lo que se autorizó antes se autorizó.
677
+ entrada.autorizadas = [];
678
+ resultado =
679
+ (await consola.correrTarea(peticion, (m) => void (motivo ??= m), (ficheros) => void entrada.autorizadas?.push(...ficheros))) ?? undefined;
680
+ terminoLimpio = true;
681
+ }
682
+ }
683
+ catch (error) {
684
+ // El `motivo ??=` conserva el primero: la consola de tarea aparca ANTES de lanzar, así
685
+ // que cuando el error llega aquí el motivo bueno ya está puesto. Y si lo que reventó
686
+ // fue la propia marca, `marcada` sigue en falso y de aquí no sale ningún estado: no hay
687
+ // ninguna tarea nuestra que contar.
688
+ //
689
+ // `entrada.cortarPedido` cuenta AQUÍ TAMBIÉN, igual que `parando`: si el turno ya
690
+ // estaba corriendo cuando alguien pidió descartarla, `cortar()` lo aborta y esa
691
+ // `AbortError` llega a este `catch` exactamente igual que un Ctrl-C — sin la
692
+ // comprobación, el motivo saldría «el turno falló (AbortError)», que es FALSO: nadie
693
+ // falló, alguien lo cortó a propósito. Medido: sin esto, el kanban enseñaría ese
694
+ // motivo engañoso durante el instante entre el corte y el borrado.
695
+ if (marcada) {
696
+ // Se marca la SITUACIÓN, no el texto: cuál de las dos verdades es depende de si el
697
+ // hilo sobrevive, y eso no se sabe hasta `olvidarSiNoSePuedeAbrir`, más abajo. El
698
+ // texto provisional es el que valdría sin borrado, y se recompone al final.
699
+ if (motivo === undefined && (parando || entrada.cortarPedido))
700
+ cortadaAMitad = true;
701
+ motivo ??= cortadaAMitad ? MOTIVO_CORTADA_POR_CIERRE : `el turno falló (${codigoDe(error)})`;
702
+ }
703
+ else
704
+ renunciarSiSigueNueva(tarea.id, error);
705
+ }
706
+ finally {
707
+ /**
708
+ * **Cerrar ANTES de escribir el estado, y el orden es load-bearing.** Solo las tareas
709
+ * `en-proceso` ocupan su proyecto (`siguientesAEjecutar`), así que si esto se aparcara
710
+ * con la sesión todavía abierta, el planificador dejaría arrancar otra tarea sobre el
711
+ * mismo proyecto — dos turnos a la vez sobre el mismo disco, git y checkpointer, que
712
+ * es exactamente lo que el cerrojo por proyecto existe para evitar. Hay test del orden.
713
+ *
714
+ * Y es también lo que VUELCA el transcript de la sesión: `cerrar()` llama a `volcar()`.
715
+ * En el `finally` porque **ningún** camino puede dejar la consola abierta: la marca que
716
+ * no se pudo escribir y el turno que revienta son un handle filtrado por tarea, y con un
717
+ * lazo corriendo eso se acumula.
718
+ */
719
+ await cortar();
720
+ }
721
+ if (!marcada) {
722
+ renunciarSiSigueNueva(tarea.id);
723
+ return;
724
+ }
725
+ // Un turno que devolvió porque le cerramos la consola no está «terminado»: se dice, con
726
+ // las mismas palabras que la reconciliación, porque es la misma situación. Se mira si
727
+ // devolvió LIMPIO y no solo si estamos parando: un turno que acaba su trabajo en el mismo
728
+ // instante en que alguien pulsa Ctrl-C acabaría con un motivo falso en el kanban.
729
+ if (motivo === undefined && !terminoLimpio) {
730
+ cortadaAMitad = true;
731
+ motivo = MOTIVO_CORTADA_POR_CIERRE;
732
+ }
733
+ /**
734
+ * **Y aquí «terminada» deja de significar «el turno acabó».**
735
+ *
736
+ * Solo se pasa por la puerta si no hay ya un motivo: una tarea que se aparcó por su
737
+ * cuenta —el agente preguntó y no había nadie, la consola se cerró a mitad— ya tiene el
738
+ * motivo que explica de verdad qué pasó, y preguntarle al juez sobre un trabajo que se
739
+ * cortó a mitad gastaría una llamada del modelo más caro para taparlo.
740
+ */
741
+ if (motivo === undefined) {
742
+ const entrega = await puertaDeEntrega(tarea, hilo, entrada, resultado, consola.esperarMarca?.bind(consola));
743
+ motivo = entrega.motivo;
744
+ // Con qué condición de MENOS se entregó, si fue el caso: viaja pegada al veredicto,
745
+ // porque una entrega con una condición menos no puede parecer una entrega normal y el
746
+ // `motivo` de una tarea terminada no existe (`conEstado` lo borra, a propósito).
747
+ if (entrega.salvedad !== undefined && entrada.veredicto !== undefined) {
748
+ entrada.veredicto = { ...entrada.veredicto, salvedad: entrega.salvedad };
749
+ }
750
+ }
751
+ /**
752
+ * El estado final, y **el camino de error pasa por lo mismo que el bueno**: la regla de
753
+ * `sesion` y la consola cerrada. Aquí ya se sabe si el turno dejó conversación —`cortar()`
754
+ * acaba de volcarla—: uno que no emitió un solo acto cierra sin volcar nada, y entonces su
755
+ * id no nombra nada abrible (ver `olvidarSiNoSePuedeAbrir`).
756
+ */
757
+ // La regla se resuelve UNA vez y fuera del `try` de la escritura: así el camino de error
758
+ // reusa la decisión en vez de volver a preguntar —y a olvidar— el mismo hilo dos veces.
759
+ //
760
+ // Y lo AUTORIZADO se compone en la misma transformación, por el mismo motivo por el que
761
+ // la regla de `sesion` vive aquí: los CUATRO finales —terminada, aparcada, el error de la
762
+ // escritura final y el último recurso de `revisar`— pasan por lo mismo, así que ninguno
763
+ // puede quedarse sin registrar lo que la tarea autorizó. Va sobre la tarea RELEÍDA del
764
+ // disco (`escribirSiSigueSiendoNuestra` le pasa la actual), no sobre la que se despachó,
765
+ // para no pisar lo que el cable cambiara entre medias.
766
+ let conLoQueQuede = (t) => conVeredicto(conAutorizadas(t, entrada.autorizadas), entrada.veredicto);
767
+ try {
768
+ if (await olvidarSiNoSePuedeAbrir(tarea.proyecto.raiz, hilo)) {
769
+ conLoQueQuede = (t) => sinSesion(conVeredicto(conAutorizadas(t, entrada.autorizadas), entrada.veredicto));
770
+ // Y ahora sí se sabe: el hilo se ha borrado, así que reintentar no continúa nada.
771
+ if (cortadaAMitad)
772
+ motivo = motivoDeCorteAMitad(true);
773
+ }
774
+ }
775
+ catch {
776
+ // Ni preguntar se pudo: no se sabe, y entonces no se borra nada.
777
+ }
778
+ try {
779
+ // Aparcada si algo pidió a una persona o si la puerta de la entrega no la dejó pasar;
780
+ // terminada si acabó limpia Y pasó la puerta. «Terminada» ya NO significa «el turno
781
+ // acabó»: significa que las tres condiciones se midieron en verde y que el juez de QA
782
+ // dijo que el trabajo hace lo que se pedía. Ver `puertaDeEntrega`.
783
+ if (motivo === undefined) {
784
+ escribirSiSigueSiendoNuestra(tarea.id, (actual) => actual.estado === "en-proceso" && actual.pid === pid
785
+ ? conEstado(conLoQueQuede(actual), "terminada")
786
+ : undefined);
787
+ }
788
+ else {
789
+ aparcar(tarea.id, motivo, conLoQueQuede);
790
+ }
791
+ }
792
+ catch (error) {
793
+ // Ni escribir el final puede dejarla «en proceso» sin nadie detrás: se intenta aparcar
794
+ // diciéndolo, con la MISMA regla ya resuelta arriba.
795
+ aparcar(tarea.id, motivo ?? `el corredor no pudo cerrarla (${codigoDe(error)})`, conLoQueQuede);
796
+ renunciarSiSigueNueva(tarea.id, error);
797
+ }
798
+ };
799
+ /**
800
+ * ¿Seguimos mandando? Se pregunta AL DISCO, al empezar cada pasada y antes de cada
801
+ * despacho, y al perderlo se para el lazo sin soltar nada.
802
+ *
803
+ * Las dos veces hacen falta y no son la misma: la de la pasada mantiene honesto
804
+ * `corriendoAqui()` —que es lo que el kanban pinta— incluso cuando no hay hueco para
805
+ * despachar nada, y la de cada despacho es la que impide abrir una consola con el
806
+ * cerrojo ya en otras manos. Y NO se llama a `soltarCerrojo`: solo borra el propio, y
807
+ * ese fichero es ya de otro proceso.
808
+ */
809
+ const sigoMandando = () => {
810
+ if (opciones.disco.sigoSiendoDueño())
811
+ return true;
812
+ if (miCerrojo)
813
+ informar("el cerrojo de las tareas lo tiene ya otro proceso: aquí se dejan de ejecutar");
814
+ miCerrojo = false;
815
+ // Perderlo es que lo tiene otro, y eso es lo que la pantalla necesita saber: aquí no
816
+ // avanza, pero allí sí — o sea que hay a quién esperar.
817
+ cerrojoAjeno = true;
818
+ return false;
819
+ };
820
+ const revisar = () => {
821
+ if (!miCerrojo || parando)
822
+ return;
823
+ if (!sigoMandando())
824
+ return;
825
+ /**
826
+ * Lo que este proceso tiene EN VUELO se le cuenta al planificador como «en proceso»,
827
+ * lo diga el disco o no, y esas dos correcciones cubren dos ventanas distintas:
828
+ * - la tarea despachada cuya marca todavía no está escrita —abrir la consola es
829
+ * asíncrono—: sin esto, dos revisiones en el mismo tick (el arranque y un `crear`
830
+ * que llega del cable) elegirían la misma tarea y abrirían dos consolas sobre el
831
+ * mismo proyecto;
832
+ * - y la que DESCARTARON del índice mientras corría: su turno sigue vivo, así que su
833
+ * hueco y su proyecto siguen ocupados hasta que devuelva.
834
+ * Contarlas por su estado, y no restando `enVuelo.size` del tope, es lo que evita
835
+ * descontarlas DOS veces (el disco ya dice «en proceso» de casi todas).
836
+ */
837
+ const enDisco = opciones.disco.listar();
838
+ const enElIndice = new Set(enDisco.map((t) => t.id));
839
+ const lista = [
840
+ ...enDisco.map((t) => (enVuelo.has(t.id) ? { ...t, estado: "en-proceso" } : t)),
841
+ ...[...enVuelo.values()]
842
+ .filter((e) => !enElIndice.has(e.tarea.id))
843
+ .map((e) => ({ ...e.tarea, estado: "en-proceso" })),
844
+ ];
845
+ for (const tarea of siguientesAEjecutar(lista.filter((t) => !renunciadas.has(t.id)), {
846
+ concurrencia: opciones.concurrencia(),
847
+ bloqueados: opciones.bloqueados?.() ?? [],
848
+ })) {
849
+ // Antes de CADA tarea, no solo al arrancar ni una vez por pasada: ver la cabecera.
850
+ if (!sigoMandando())
851
+ return;
852
+ /**
853
+ * La entrada se registra ANTES de arrancar el trabajo, y su `cortar` lo rellena
854
+ * `correr` en cuanto tiene la consola: hasta ese momento no hay nada que cortar, y un
855
+ * `parar()` que llegue en esa ventana lo ve el propio `correr` y no arranca el turno.
856
+ * `cortarPedido` nace en `false` por lo mismo: es la marca que sobrevive esa misma
857
+ * ventana cuando quien pide el corte es `Corredor.cortar(id)` y no `parar()`.
858
+ */
859
+ // Sin `autorizadas`: no está puesta hasta que el turno arranca, que es lo que
860
+ // distingue «no consta» de «no autorizó nada».
861
+ const entrada = {
862
+ tarea,
863
+ trabajo: Promise.resolve(),
864
+ cortar: async () => { },
865
+ cortarPedido: false,
866
+ mirones: new Set(),
867
+ };
868
+ enVuelo.set(tarea.id, entrada);
869
+ entrada.trabajo = correr(tarea, entrada)
870
+ // Nada de `informar`: lo que le pasa a una tarea va a su registro. Si `correr` se
871
+ // rompe fuera del turno, la tarea se queda aparcada diciéndolo — nunca «en proceso»
872
+ // sin nadie detrás.
873
+ .catch(async (error) => {
874
+ // El último recurso, y pasa por la MISMA regla de `sesion` que el camino bueno:
875
+ // dejar escrito aquí un id que no nombra nada abrible sería la regla cumpliéndose
876
+ // solo cuando todo va bien.
877
+ await aparcarMirandoLaSesion(tarea, `el corredor no pudo con ella (${codigoDe(error)})`, entrada.autorizadas, entrada.veredicto).catch(() => undefined);
878
+ // Y si tras intentarlo SIGUE en `nuevo`, este proceso no puede moverla de sitio:
879
+ // renuncia, o el `revisar()` de abajo la vuelve a coger en el acto y otra vez.
880
+ // Medido: sin esto, un índice que no se puede escribir es una CPU al 100%.
881
+ renunciarSiSigueNueva(tarea.id, error);
882
+ })
883
+ .finally(() => {
884
+ enVuelo.delete(tarea.id);
885
+ revisar();
886
+ });
887
+ }
888
+ };
889
+ /** El arranque de verdad. Lo envuelve `arrancar`, que es quien no puede lanzar. */
890
+ const arrancarDeVerdad = async () => {
891
+ const cerrojo = opciones.disco.tomarCerrojo();
892
+ // Lo que se apunta es lo MEDIDO, y en este orden: si esto lanza, `cerrojoAjeno` se queda
893
+ // sin afirmar nada, que es la verdad — no se ha podido leer quién lo tiene.
894
+ cerrojoAjeno = !cerrojo.tomado;
895
+ if (!cerrojo.tomado) {
896
+ miCerrojo = false;
897
+ informar(`las tareas las ejecuta otro proceso (pid ${cerrojo.dePid}); aquí solo se ven`);
898
+ return;
899
+ }
900
+ miCerrojo = true;
901
+ /**
902
+ * RECONCILIACIÓN. Toda tarea «en proceso» se aparca, **sin mirar el pid**.
903
+ *
904
+ * La cola sobrevive a parar el proceso, pero el turno en vuelo no, y al ARRANCAR este
905
+ * proceso no tiene ninguna tarea en vuelo por construcción: así que toda «en proceso»
906
+ * que haya en disco es vieja, y dejarla así sería afirmar lo que no se sabe. Mirar el
907
+ * pid —«las que no son mías»— parece más fino y es un agujero medido: los pid se
908
+ * REUSAN, y la tarea del proceso muerto que casualmente tenía este número se quedaba
909
+ * «en proceso» para siempre, sin nadie ejecutándola, sin transición posible desde ahí
910
+ * y con su proyecto ocupado para el planificador.
911
+ *
912
+ * Solo con el cerrojo NUESTRO: si lo tiene otro proceso, sus «en proceso» están
913
+ * corriendo de verdad y aparcarlas sería matarlas desde fuera.
914
+ */
915
+ const lista = opciones.disco.listar();
916
+ const reconciliada = [];
917
+ for (const t of lista) {
918
+ if (t.estado !== "en-proceso") {
919
+ reconciliada.push(t);
920
+ continue;
921
+ }
922
+ // El hilo de un turno cortado a mitad no nombra nada abrible: se olvida antes de
923
+ // aparcar. Ver `conSesionSoloSiSePuedeAbrir`.
924
+ const limpiar = await olvidarSiNoSePuedeAbrir(t.proyecto.raiz, t.sesion);
925
+ // El motivo se compone CON lo que acaba de decidirse: si el hilo se olvidó, decir que
926
+ // el agente lo recuerda sería falso justo en la rama que lo borró.
927
+ reconciliada.push(conEstado(limpiar ? sinSesion(t) : t, "requiere-atencion", motivoDeReconciliacion(t.pid, limpiar)));
928
+ }
929
+ if (reconciliada.some((t, i) => t !== lista[i])) {
930
+ opciones.disco.guardar(reconciliada);
931
+ opciones.alCambiar?.(reconciliada);
932
+ }
933
+ /**
934
+ * SIEMBRA de la marca de tarea en las sesiones que ya existían. Va con la lista
935
+ * RECONCILIADA porque una `en-proceso` puede haber perdido su `sesion` justo arriba (el
936
+ * hilo que no se puede abrir se olvida), y sembrar la que se acaba de olvidar dejaría
937
+ * marcada una fila que ya no existe.
938
+ *
939
+ * Aquí el cruce con la cola es correcto y en el cable sería un fallo abierto, y la
940
+ * diferencia es cuál es la pregunta: allí es «¿es esto de una tarea?», y sin cola la
941
+ * respuesta sería «no» en silencio; aquí es «marca estas, que sé que lo son», leyendo
942
+ * la autoridad. Y solo AÑADE: `marcarTareaDeSesion` no pisa ninguna marca ni la quita.
943
+ *
944
+ * Un fallo sembrando no puede tumbar el arranque: la marca es cosmética al lado de
945
+ * ejecutar la cola. Se traga por tarea, así que una entrada rara no se lleva las demás.
946
+ */
947
+ if (opciones.marcarSesionDeTarea !== undefined) {
948
+ for (const t of reconciliada) {
949
+ if (t.sesion === undefined)
950
+ continue;
951
+ try {
952
+ opciones.marcarSesionDeTarea(t.proyecto.raiz, t.sesion, t.id);
953
+ }
954
+ catch {
955
+ // Un índice roto o ilegible: la fila se queda sin marcar, que es lo de antes.
956
+ }
957
+ }
958
+ }
959
+ revisar();
960
+ };
961
+ return {
962
+ async arrancar() {
963
+ /**
964
+ * Un fallo aquí NO puede tumbar la consola web, que es la misma regla que ya siguen la
965
+ * conexión con CloudStudio y la apertura del navegador: las tareas de fondo son una
966
+ * pieza más, y quien está delante viene a trabajar en su proyecto. `tomarCerrojo`
967
+ * lanza ante cualquier cosa que no sea `EEXIST`/`ENOENT` —un `~/.xonecode/tareas` que
968
+ * no se puede escribir— y la reconciliación escribe el índice, así que las dos pueden
969
+ * reventar. Se dice con el código (esto es del PROCESO, no de ninguna tarea: es el
970
+ * canal del aviso del cerrojo) y este proceso se queda sin ejecutar, que es lo mismo
971
+ * que le pasa al segundo.
972
+ */
973
+ try {
974
+ await arrancarDeVerdad();
975
+ }
976
+ catch (error) {
977
+ /**
978
+ * **Y se SUELTA el cerrojo, no solo se olvida.** Poner `miCerrojo = false` y dejar el
979
+ * fichero en disco era el peor fallo posible de esta pieza: `corriendoAqui()` mentía,
980
+ * `parar()` no soltaba nada —creía que no lo tenía— y las tareas de TODA la máquina se
981
+ * quedaban congeladas hasta que el proceso muriera. Un cerrojo que existe para que no
982
+ * haya dos corredores acabando con que no haya ninguno.
983
+ *
984
+ * Llamarlo es seguro aunque el fallo ocurriera ANTES de tomarlo: `soltarCerrojo` solo
985
+ * borra el cerrojo cuyo pid es el nuestro.
986
+ */
987
+ try {
988
+ opciones.disco.soltarCerrojo();
989
+ }
990
+ catch {
991
+ // Si tampoco se puede soltar, no hay nada más que hacer: lo de abajo lo dice.
992
+ }
993
+ miCerrojo = false;
994
+ informar(`las tareas de fondo no se pueden ejecutar aquí (${codigoDe(error)}); la consola sigue`);
995
+ }
996
+ },
997
+ async parar() {
998
+ parando = true;
999
+ /**
1000
+ * Se CORTA lo que esté en vuelo, no se espera: un turno tarda minutos y quien pulsa
1001
+ * Ctrl-C espera que el proceso se vaya. Cerrar la consola es lo que hace que el turno
1002
+ * devuelva (aborta el stream del grafo), y el `finally` de `correr` la aparca diciendo
1003
+ * qué pasó. Se cortan TODAS antes de esperar a ninguna: al revés, la última esperaría
1004
+ * a que la primera acabara de cerrarse.
1005
+ */
1006
+ const cortando = [...enVuelo.values()].map((e) => e.cortar());
1007
+ const trabajos = [...enVuelo.values()].map((e) => e.trabajo);
1008
+ const cuantas = trabajos.length;
1009
+ /**
1010
+ * Con PLAZO, y el plazo cubre las dos esperas: el cierre de la consola puede colgarse
1011
+ * igual que el turno. Al agotarse **no se miente** — la tarea se queda «en proceso» y
1012
+ * la reconciliación del siguiente arranque la aparca, que es exactamente para lo que
1013
+ * existe. Se DICE, porque una tarea que se queda diciendo «en proceso» sin nadie
1014
+ * detrás y sin explicación es el estado más confuso que puede quedar en el kanban.
1015
+ */
1016
+ const aTiempo = await conPlazo((async () => {
1017
+ await Promise.allSettled(cortando);
1018
+ await Promise.allSettled(trabajos);
1019
+ })(), opciones.esperaAlParar ?? TOPE_DE_PARADA_MS);
1020
+ if (!aTiempo) {
1021
+ informar(`${cuantas} tarea(s) no han devuelto al cerrar: se quedan «en proceso» y el próximo ` +
1022
+ `arranque las reconcilia`);
1023
+ }
1024
+ // El cerrojo se suelta igual: este proceso se va, y dejarlo puesto obligaría al
1025
+ // siguiente a recogerlo por el camino que no es atómico.
1026
+ if (miCerrojo)
1027
+ opciones.disco.soltarCerrojo();
1028
+ miCerrojo = false;
1029
+ },
1030
+ /**
1031
+ * `parar()` acotado a UNA tarea. Ver el docblock de `Corredor.cortar` para el contrato
1032
+ * completo; aquí solo el porqué de cada línea.
1033
+ */
1034
+ async cortar(id) {
1035
+ const entrada = enVuelo.get(id);
1036
+ // No corre AQUÍ: ni hueco que liberar ni consola que cerrar. Puede que no corra en
1037
+ // ningún sitio, o que corra en OTRO proceso —el dashboard de un segundo corredor no
1038
+ // puede alcanzarlo, y ese límite lo pone el sistema operativo, no esta función—, pero
1039
+ // ninguno de los dos casos es «espera»: son «aquí no hay nada que hacer».
1040
+ if (entrada === undefined)
1041
+ return true;
1042
+ // La marca sobrevive aunque `entrada.cortar` todavía sea el de mentira de `revisar()`
1043
+ // —la tarea sigue en `nuevo`, la consola no ha terminado de abrirse—: es la ventana
1044
+ // que el docblock de `EnVuelo.cortarPedido` mide, y `correr()` la mira en cuanto abre.
1045
+ entrada.cortarPedido = true;
1046
+ // Se DISPARA el corte —si la consola ya está abierta, esto es lo que aborta el turno
1047
+ // de verdad— y se espera a `trabajo` ENTERO, no solo a que el corte dispare: solo
1048
+ // cuando `trabajo` se asienta es cierto que `correr()` pasó por su `finally` y la
1049
+ // consola soltó `/adjuntos/`. El mismo plazo que `parar()`, y por el mismo motivo: un
1050
+ // cierre puede colgarse igual que un turno.
1051
+ return conPlazo((async () => {
1052
+ await entrada.cortar();
1053
+ await entrada.trabajo;
1054
+ })(), opciones.esperaAlParar ?? TOPE_DE_PARADA_MS);
1055
+ },
1056
+ mirar(id, enviar) {
1057
+ const entrada = enVuelo.get(id);
1058
+ // No corre AQUÍ. `undefined` y no `[]`: una lista vacía diría «corre y todavía no ha
1059
+ // hecho nada», que es otra cosa — y quien pregunta necesita distinguirlas para no
1060
+ // dejar una pantalla esperando actos de un turno que no existe.
1061
+ if (entrada === undefined)
1062
+ return undefined;
1063
+ entrada.mirones.add(enviar);
1064
+ // Con la consola ya abierta, el transcript de ese instante. Sin ella —la ventana de la
1065
+ // apertura— una lista vacía y el enganche apuntado: `correr()` lo cumple con una
1066
+ // reemisión en cuanto la consola existe.
1067
+ return entrada.consola?.mirar?.(enviar) ?? [];
1068
+ },
1069
+ dejarDeMirar(id, enviar) {
1070
+ const entrada = enVuelo.get(id);
1071
+ // La tarea puede haber terminado entre que se pulsó y que llegó el mensaje: no es un
1072
+ // error, es que ya no hay de qué desengancharse.
1073
+ if (entrada === undefined)
1074
+ return;
1075
+ entrada.mirones.delete(enviar);
1076
+ entrada.consola?.dejarDeMirar?.(enviar);
1077
+ },
1078
+ corriendoAqui: () => miCerrojo,
1079
+ // La otra mitad, y nunca deducida de la primera: ver `Corredor.ejecutaOtroProceso`.
1080
+ ejecutaOtroProceso: () => cerrojoAjeno,
1081
+ revisar,
1082
+ /**
1083
+ * Deja que se asiente lo lanzado. Vive en producción y no en el test porque solo el
1084
+ * corredor sabe qué está en vuelo, y las tres vueltas de `setImmediate` son lo que
1085
+ * drena las microtareas Y las devoluciones de E/S que un turno de mentira encadena.
1086
+ * No es el reloj del lazo: el lazo no tiene ninguno — se revisa por evento.
1087
+ */
1088
+ async asentar() {
1089
+ for (let i = 0; i < 3; i += 1)
1090
+ await new Promise((r) => setImmediate(r));
1091
+ },
1092
+ };
1093
+ }
1094
+ /**
1095
+ * Espera una promesa con plazo. Devuelve si llegó a tiempo.
1096
+ *
1097
+ * El temporizador se limpia SIEMPRE: uno vivo mantiene el proceso en pie, y esto corre justo
1098
+ * en el camino por el que el proceso se está yendo — un `setTimeout` de treinta segundos sin
1099
+ * `clearTimeout` haría que el Ctrl-C tardara treinta segundos aunque todo hubiera cerrado ya.
1100
+ */
1101
+ async function conPlazo(promesa, ms) {
1102
+ let temporizador;
1103
+ const plazo = new Promise((resuelto) => {
1104
+ temporizador = setTimeout(() => resuelto(false), ms);
1105
+ });
1106
+ try {
1107
+ return await Promise.race([promesa.then(() => true), plazo]);
1108
+ }
1109
+ finally {
1110
+ if (temporizador !== undefined)
1111
+ clearTimeout(temporizador);
1112
+ }
1113
+ }
1114
+ /** Una línea, nunca una traza con rutas de la máquina. */
1115
+ function unaLinea(error) {
1116
+ const mensaje = error instanceof Error ? error.message : String(error);
1117
+ return mensaje.split(/\r?\n/)[0].slice(0, 160);
1118
+ }
1119
+ //# sourceMappingURL=corredorDeTareas.js.map