xonecode 0.14.1 → 0.17.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 (57) hide show
  1. package/THIRD_PARTY_NOTICES.md +2 -2
  2. package/apps/web/dist/assets/index-CnskHXof.js +485 -0
  3. package/apps/web/dist/assets/index-DLfF2uEK.css +1 -0
  4. package/apps/web/dist/assets/{php-KZMIPXrN.js → php-8TR3jUGL.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-jf5bWc_X.js → ruby-IynpAZS-.js} +1 -1
  6. package/apps/web/dist/index.html +2 -2
  7. package/dist/agent/conectores/gestorJira.js +164 -0
  8. package/dist/agent/conectores/gestorJira.js.map +1 -0
  9. package/dist/agent/conectores/gestorNotion.js +365 -0
  10. package/dist/agent/conectores/gestorNotion.js.map +1 -0
  11. package/dist/agent/conectores/servicioDeConectores.js +135 -34
  12. package/dist/agent/conectores/servicioDeConectores.js.map +1 -1
  13. package/dist/agent/config/configEnDisco.js +28 -0
  14. package/dist/agent/config/configEnDisco.js.map +1 -1
  15. package/dist/agent/config/selectorEnMaquina.js +9 -2
  16. package/dist/agent/config/selectorEnMaquina.js.map +1 -1
  17. package/dist/agent/config/settingsEnDisco.js +41 -1
  18. package/dist/agent/config/settingsEnDisco.js.map +1 -1
  19. package/dist/agent/grafo/marcarCriteriosDelPlan.js +85 -0
  20. package/dist/agent/grafo/marcarCriteriosDelPlan.js.map +1 -0
  21. package/dist/agent/grafo/xoneAgent.js +4 -0
  22. package/dist/agent/grafo/xoneAgent.js.map +1 -1
  23. package/dist/agent/motores/trueforge/sesionTrueforge.js +3 -0
  24. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  25. package/dist/agent/sesiones/checkpointer.js +18 -0
  26. package/dist/agent/sesiones/checkpointer.js.map +1 -1
  27. package/dist/agent/sesiones/modeloDelCambio.js +5 -2
  28. package/dist/agent/sesiones/modeloDelCambio.js.map +1 -1
  29. package/dist/agent/sesiones/sesionGit.js +5 -1
  30. package/dist/agent/sesiones/sesionGit.js.map +1 -1
  31. package/dist/agent/subagentes/agentesEnDisco.js +13 -4
  32. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  33. package/dist/core/config.js +71 -0
  34. package/dist/core/config.js.map +1 -1
  35. package/dist/core/gestorDeTareas.js +202 -0
  36. package/dist/core/gestorDeTareas.js.map +1 -0
  37. package/dist/core/ports.js +43 -0
  38. package/dist/core/ports.js.map +1 -1
  39. package/dist/core/selectorDeCarpeta.js +12 -2
  40. package/dist/core/selectorDeCarpeta.js.map +1 -1
  41. package/dist/core/settings.js +38 -4
  42. package/dist/core/settings.js.map +1 -1
  43. package/dist/core/tareasDelPlan.js +66 -0
  44. package/dist/core/tareasDelPlan.js.map +1 -1
  45. package/dist/web/servidor/arranque.js +840 -36
  46. package/dist/web/servidor/arranque.js.map +1 -1
  47. package/dist/web/servidor/datosDeCierre.js +37 -0
  48. package/dist/web/servidor/datosDeCierre.js.map +1 -0
  49. package/dist/web/servidor/sesiones.js +20 -0
  50. package/dist/web/servidor/sesiones.js.map +1 -1
  51. package/dist/web/servidor/transporte.js.map +1 -1
  52. package/dist/web/servidor/vestibulo.js +79 -5
  53. package/dist/web/servidor/vestibulo.js.map +1 -1
  54. package/package.json +1 -1
  55. package/skills/xone-plan-builder/SKILL.md +7 -2
  56. package/apps/web/dist/assets/index-CWAPXNAO.css +0 -1
  57. package/apps/web/dist/assets/index-KJGPIrNa.js +0 -485
@@ -55,18 +55,18 @@ import { PLATAFORMAS_DE_DISPOSITIVO } from "../../core/settings.js";
55
55
  import { instalarHerramientaDeDispositivos, verificarDispositivo } from "../../agent/dispositivos/dispositivosEnMaquina.js";
56
56
  import { arrancarEmulador } from "../../agent/dispositivos/arranqueDeEmulador.js";
57
57
  import { correrPasoDeReceta } from "../../agent/dispositivos/instalacionEnMaquina.js";
58
- import { abrirCarpetaDelSistema } from "../../agent/config/selectorEnMaquina.js";
58
+ import { abrirCarpetaDelSistema, abrirDirectorioDelSistema } from "../../agent/config/selectorEnMaquina.js";
59
59
  import { modelosDeMotor } from "../../agent/config/modelosDeMotor.js";
60
60
  import { parsear, compatibleConOpenAi, esProveedorPersonalizado, idDeProveedorPersonalizado, motivoDeEndpointInaceptable, motivoDeSlugInaceptable, nombreDeProveedor, PROVEEDORES, slugDesdeNombre, resolver, SIN_CREDENCIAL, PAPELES, } from "../../core/modelos.js";
61
61
  import { hayCredencial } from "../../cli/consola.js";
62
62
  import { motivoDeClaveInaceptable } from "../../core/config.js";
63
- import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarModeloGlobal, } from "../../agent/config/configEnDisco.js";
63
+ import { aplicarAuth, aplicarCredencialAlProceso, cargar, guardarConectoresDeProyecto, guardarGestorDeProyecto, guardarModeloGlobal, } from "../../agent/config/configEnDisco.js";
64
64
  import { borrarCredencial, guardarCredencial } from "../../agent/config/authEnDisco.js";
65
65
  import { borrarProveedorPersonalizado, guardarProveedorPersonalizado, proveedoresPersonalizados, } from "../../agent/config/configEnDisco.js";
66
66
  import { crearCheckpointerDeProyecto, mantenimientoDelCheckpointer, } from "../../agent/sesiones/checkpointer.js";
67
67
  import { resumenDePoda } from "../../core/podaDeCheckpoint.js";
68
68
  import { cargarSettings, guardarConcurrenciaDeTareas, guardarDepurar, guardarWorkspace as guardarWorkspaceEnDisco, guardarDispositivos, guardarEntorno as guardarEntornoEnDisco, olvidarEntornoDeSettings, copiasDeEntorno, borrarCopiasDeEntorno, } from "../../agent/config/settingsEnDisco.js";
69
- import { dentroDelWorkspace, depuracionActiva, expandirConCasa, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
69
+ import { dentroDelWorkspace, depuracionActiva, expandirConCasa, mismaRuta, motivoParaNoOlvidarEntorno, motivoDeNombreDeEntornoInaceptable, motivoDeWorkspaceInaceptable, } from "../../core/settings.js";
70
70
  import { esModoDeEscritura, MODO_POR_OMISION, } from "../../core/modoDeEscritura.js";
71
71
  import { mudarWorkspaceLegado } from "../../agent/config/mudanzaEnDisco.js";
72
72
  import { elegirCarpetaEnMaquina, haySelectorDeCarpeta } from "../../agent/config/selectorEnMaquina.js";
@@ -107,8 +107,12 @@ import { baseDeWorkspaceLegada, baseDeWorkspacePorOmision, conexionDeVestibulo,
107
107
  // doblan.
108
108
  import { filaDeTarea } from "./transporte.js";
109
109
  import { esEsfuerzo, nivelesDeEsfuerzo } from "../../core/esfuerzo.js";
110
- import { definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
110
+ import { CATALOGO_DE_CONECTORES, definicionDelCable, RUTA_CALLBACK_MCP } from "../../core/conectores.js";
111
111
  import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeConectores.js";
112
+ import { crearGestorJira } from "../../agent/conectores/gestorJira.js";
113
+ import { crearGestorNotion } from "../../agent/conectores/gestorNotion.js";
114
+ import { admiteMiasDelVinculo, motivoDeProyectoInaceptable, transicionPropuesta, comentarioDeCierre, } from "../../core/gestorDeTareas.js";
115
+ import { datosDeCierre } from "./datosDeCierre.js";
112
116
  /** Las dos rutas del cable. El cliente las tiene escritas en `apps/web/src/conexion.ts`. */
113
117
  /**
114
118
  * Cuánto se espera al aviso de «lo que ya había sin commitear» antes de anunciar sin él.
@@ -208,6 +212,86 @@ function tipoServido(mime) {
208
212
  function escaparHtml(texto) {
209
213
  return texto.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
210
214
  }
215
+ /**
216
+ * El texto con cada cosa con forma de CORREO cambiada por «[correo]». Para lo que cruza el cable
217
+ * desde el gestor de tareas: ni correos por el cable (IXCODE-11), y el texto de error de una tool
218
+ * de Jira es suyo, no nuestro. La forma es deliberadamente ANCHA —algo@algo.algo sin espacios—:
219
+ * tachar de más en un mensaje de error cuesta poco; dejar pasar un correo, no.
220
+ */
221
+ export function sinCorreos(texto) {
222
+ return texto.replace(/[^\s@<>()"'«»]+@[^\s@<>()"'«»]+\.[^\s@<>()"'«»]+/g, "[correo]");
223
+ }
224
+ /**
225
+ * El mensaje `gestor` con los campos que su ACCIÓN pide, o `undefined`. Llega del navegador sin
226
+ * tipos: `atenderGestor` confía en esta forma, así que se comprueba aquí y no dentro.
227
+ */
228
+ function gestorBienFormado(m) {
229
+ const x = m;
230
+ const texto = (k) => typeof x[k] === "string";
231
+ switch (x.accion) {
232
+ case "estado":
233
+ return { clase: "gestor", accion: "estado" };
234
+ case "desvincular":
235
+ return { clase: "gestor", accion: "desvincular" };
236
+ case "sitios":
237
+ return texto("conector") ? { clase: "gestor", accion: "sitios", conector: x.conector } : undefined;
238
+ case "proyectos":
239
+ return texto("conector") && texto("sitio")
240
+ ? { clase: "gestor", accion: "proyectos", conector: x.conector, sitio: x.sitio }
241
+ : undefined;
242
+ case "buscarProyectos":
243
+ return texto("conector") && texto("texto")
244
+ ? { clase: "gestor", accion: "buscarProyectos", conector: x.conector, texto: x.texto }
245
+ : undefined;
246
+ case "describir":
247
+ return texto("conector") && texto("proyecto")
248
+ ? { clase: "gestor", accion: "describir", conector: x.conector, proyecto: x.proyecto }
249
+ : undefined;
250
+ case "vincular":
251
+ return texto("conector") && texto("sitio") && texto("proyecto")
252
+ ? { clase: "gestor", accion: "vincular", conector: x.conector, sitio: x.sitio, proyecto: x.proyecto }
253
+ : undefined;
254
+ case "usarConector":
255
+ return texto("conector") && typeof x.usar === "boolean"
256
+ ? { clase: "gestor", accion: "usarConector", conector: x.conector, usar: x.usar }
257
+ : undefined;
258
+ case "pendientes":
259
+ if (x.texto !== undefined && !texto("texto"))
260
+ return undefined;
261
+ if (x.mias !== undefined && typeof x.mias !== "boolean")
262
+ return undefined;
263
+ return {
264
+ clase: "gestor",
265
+ accion: "pendientes",
266
+ ...(x.texto === undefined ? {} : { texto: x.texto }),
267
+ ...(x.mias === true ? { mias: true } : {}),
268
+ };
269
+ case "ficha":
270
+ return texto("clave") ? { clase: "gestor", accion: "ficha", clave: x.clave } : undefined;
271
+ case "transiciones":
272
+ return texto("clave") && (x.para === "empezar" || x.para === "cerrar")
273
+ ? { clase: "gestor", accion: "transiciones", clave: x.clave, para: x.para }
274
+ : undefined;
275
+ case "empezar": {
276
+ if (!texto("clave"))
277
+ return undefined;
278
+ if (x.transicion !== undefined && !texto("transicion"))
279
+ return undefined;
280
+ return { clase: "gestor", accion: "empezar", clave: x.clave, ...(x.transicion === undefined ? {} : { transicion: x.transicion }) };
281
+ }
282
+ case "borradorDeCierre":
283
+ return { clase: "gestor", accion: "borradorDeCierre" };
284
+ case "cerrar": {
285
+ if (!texto("comentario"))
286
+ return undefined;
287
+ if (x.transicion !== undefined && !texto("transicion"))
288
+ return undefined;
289
+ return { clase: "gestor", accion: "cerrar", comentario: x.comentario, ...(x.transicion === undefined ? {} : { transicion: x.transicion }) };
290
+ }
291
+ default:
292
+ return undefined;
293
+ }
294
+ }
211
295
  /**
212
296
  * Monta `/eventos` y `/accion` sobre un servidor ya levantado.
213
297
  *
@@ -279,6 +363,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
279
363
  let proyectoElegido;
280
364
  let proyectos = [];
281
365
  let ramas = [];
366
+ /**
367
+ * De QUÉ proyecto son las `ramas` (su id). Viaja con ellas en el alta: sin esto la ventana de
368
+ * sesión nueva pintaba las ramas del ÚLTIMO proyecto preguntado mientras llegaban las del
369
+ * nuevo, y se podía empezar con la rama de otro proyecto. Se asigna DESPUÉS de la espera,
370
+ * junto a las ramas, y se vacía ANTES: una consulta que falla no deja la pareja cruzada.
371
+ */
372
+ let ramasDe;
282
373
  /**
283
374
  * El paso de cuenta ya conducido en ESTE proceso. Hace falta porque `origenDeTrabajo` se
284
375
  * congela al construir el vestíbulo: `pasosPendientes()` seguiría diciendo «cuenta»
@@ -448,6 +539,8 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
448
539
  // Solo si el entorno lo dice: ausente es «no lo he elegido», y el cliente aplica su
449
540
  // omisión. Mandar `[]` en su lugar sería decir «ninguno», que es otra cosa.
450
541
  ...(e.proyectos === undefined ? {} : { proyectos: [...e.proyectos] }),
542
+ // Los fijados, con la misma regla: sin lista no viaja nada.
543
+ ...(e.fijados === undefined || e.fijados.length === 0 ? {} : { fijados: [...e.fijados] }),
451
544
  // Cuántas copias tiene bajadas: lo que la casilla de «borrarlas también» cuenta.
452
545
  ...(() => {
453
546
  const copias = copiasDeEntorno(opciones.workspace?.() ?? baseDeWorkspacePorOmision(), e.id);
@@ -481,9 +574,20 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
481
574
  // qué enseña la ventana de sesión nueva, y decidirlo en el cliente exigiría
482
575
  // que supiera dónde vive la copia local.
483
576
  ...(hayCopiaLocal(p.nombre) ? { local: true } : {}),
577
+ // La rama de la que se BAJÓ la copia (`config.json`), que la barra pinta debajo del
578
+ // nombre. Solo con copia: sin ella no hay rama de nadie, y leer el fichero de una
579
+ // carpeta a medias diría la de un alta que no terminó.
580
+ ...(() => {
581
+ if (!hayCopiaLocal(p.nombre))
582
+ return {};
583
+ const raiz = raizDeProyectoOSilencio(p.nombre);
584
+ const rama = raiz === undefined ? undefined : cloudstudioDelProyecto(raiz)?.rama;
585
+ return typeof rama === "string" && rama !== "" ? { rama } : {};
586
+ })(),
484
587
  };
485
588
  }),
486
589
  ramas,
590
+ ...(ramasDe === undefined || ramas.length === 0 ? {} : { ramasDe }),
487
591
  proyectoAbierto,
488
592
  ...(modo === undefined ? {} : { modo }),
489
593
  // Solo «XOneCode X.X.X», para el pie de la barra: ahí no hay sitio para el commit ni
@@ -1830,6 +1934,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1830
1934
  ...(s.tarea === undefined ? {} : { deTarea: true }),
1831
1935
  ...(trabajando.has(s.id) ? { trabajando: true } : {}),
1832
1936
  ...(s.consumo === undefined ? {} : { consumo: s.consumo }),
1937
+ // Solo la CLAVE: el resto del ticket (conector, sitio) se queda en el host hasta
1938
+ // que algo lo necesite (`SesionDelCable.ticket`). `?.` y no `===undefined`: un
1939
+ // índice tocado a mano puede traer `ticket: null`, y esto no es sitio para tumbar
1940
+ // la lista ENTERA de sesiones del proyecto por una entrada mal formada.
1941
+ ...(typeof s.ticket?.clave === "string" ? { ticket: s.ticket.clave } : {}),
1942
+ // IXCODE-15: y el CONECTOR del ticket, que es con quien se cierra (`cerrar` usa
1943
+ // `ticket.conector`, no el gestor vinculado ahora): «Cerrar en Jira» en una sesión de una
1944
+ // tarea de Jira aunque el proyecto ya esté vinculado a Notion. El sitio sigue en el host.
1945
+ ...(typeof s.ticket?.clave === "string" && typeof s.ticket.conector === "string" ? { ticketConector: s.ticket.conector } : {}),
1833
1946
  }));
1834
1947
  }
1835
1948
  catch {
@@ -2048,6 +2161,45 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2048
2161
  const anunciarAbriendo = (que) => {
2049
2162
  emitir(que === undefined ? { clase: "abriendo", activo: false } : { clase: "abriendo", activo: true, ...que });
2050
2163
  };
2164
+ /**
2165
+ * Lo que sigue a abrir una sesión desde el cable, sea la que la persona pidió en la barra
2166
+ * (`atenderSesion`) o la que abre «Empezar» en el panel del proyecto (`atenderGestor`). Una
2167
+ * sola copia: dos caminos que abren sesión con dos colas distintas dejarían a uno sin el
2168
+ * contador limpio o sin mirar la cola de tareas.
2169
+ */
2170
+ const trasAbrirSesion = () => {
2171
+ // El cable se muda a la consola del proyecto, como en el alta: sin esto el usuario
2172
+ // mira un transcript vivo cuyas aprobaciones se rechazan solas al otro lado.
2173
+ adjuntar();
2174
+ /**
2175
+ * Y el contador se REEMITE, porque el gasto es de UNA conversación.
2176
+ *
2177
+ * `consumo` solo viaja cuando el consumo CAMBIA (`alCambiarConsumo`), y abrir otra
2178
+ * sesión no cambia nada: cambia de quién es la cuenta. Sin esto —medido en la pantalla
2179
+ * del usuario— la caja seguía enseñando los tokens de la conversación anterior hasta la
2180
+ * primera llamada al modelo de la nueva, que puede tardar.
2181
+ *
2182
+ * **No se usa `emitirConsumo()`, y ese es el detalle que costó el primer intento**: esa
2183
+ * función CALLA cuando no consta la cuenta, y al abrir no consta — el `SesionReal` no se
2184
+ * registra hasta que el primer turno lo construye. O sea que justo en el instante que
2185
+ * hay que limpiar, la vía normal no manda nada. Aquí el cero SÍ está medido: una
2186
+ * conversación recién abierta no ha gastado nada, y su tracker nace a cero aunque la
2187
+ * sesión sea vieja. El cliente no pinta un cero, así que el contador desaparece.
2188
+ *
2189
+ * Y si la cuenta SÍ consta —volver al foco de una consola que está corriendo un turno—
2190
+ * se manda la de verdad: mandar cero ahí borraría un contador vivo a mitad de turno.
2191
+ */
2192
+ const alAbrir = vestibulo.consumoDeSesion();
2193
+ emitir({
2194
+ clase: "consumo",
2195
+ modelo: alAbrir?.modelo ?? SIN_CONSUMO,
2196
+ externo: alAbrir?.externo ?? SIN_CONSUMO,
2197
+ ventana: ventanaDeAhora(alAbrir?.contexto ?? 0),
2198
+ });
2199
+ // Y la cola se vuelve a mirar: este proyecto queda bloqueado para las tareas —gana la
2200
+ // persona— y el que estuviera abierto antes acaba de quedar libre.
2201
+ opciones.revisarTareas?.();
2202
+ };
2051
2203
  const atenderSesion = async (peticion) => {
2052
2204
  aviso = undefined;
2053
2205
  // Antes de la primera espera, no después: entre el clic y aquí no hay nada que pintar.
@@ -2071,41 +2223,14 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2071
2223
  proyectoElegido = peticion.proyecto;
2072
2224
  // La identidad ENTERA, no el id: el servidor abre por nombre. `identidad` ya está
2073
2225
  // resuelta unas líneas más arriba contra el listado.
2226
+ ramas = [];
2227
+ ramasDe = undefined;
2074
2228
  ramas = await vestibulo.ramasDe(entornoElegido, identidad ?? peticion.proyecto);
2229
+ ramasDe = peticion.proyecto;
2075
2230
  return;
2076
2231
  }
2077
2232
  await vestibulo.abrirProyecto({ raiz, ...(peticion.sesion === undefined ? {} : { sesion: peticion.sesion }) });
2078
- // El cable se muda a la consola del proyecto, como en el alta: sin esto el usuario
2079
- // mira un transcript vivo cuyas aprobaciones se rechazan solas al otro lado.
2080
- adjuntar();
2081
- /**
2082
- * Y el contador se REEMITE, porque el gasto es de UNA conversación.
2083
- *
2084
- * `consumo` solo viaja cuando el consumo CAMBIA (`alCambiarConsumo`), y abrir otra
2085
- * sesión no cambia nada: cambia de quién es la cuenta. Sin esto —medido en la pantalla
2086
- * del usuario— la caja seguía enseñando los tokens de la conversación anterior hasta la
2087
- * primera llamada al modelo de la nueva, que puede tardar.
2088
- *
2089
- * **No se usa `emitirConsumo()`, y ese es el detalle que costó el primer intento**: esa
2090
- * función CALLA cuando no consta la cuenta, y al abrir no consta — el `SesionReal` no se
2091
- * registra hasta que el primer turno lo construye. O sea que justo en el instante que
2092
- * hay que limpiar, la vía normal no manda nada. Aquí el cero SÍ está medido: una
2093
- * conversación recién abierta no ha gastado nada, y su tracker nace a cero aunque la
2094
- * sesión sea vieja. El cliente no pinta un cero, así que el contador desaparece.
2095
- *
2096
- * Y si la cuenta SÍ consta —volver al foco de una consola que está corriendo un turno—
2097
- * se manda la de verdad: mandar cero ahí borraría un contador vivo a mitad de turno.
2098
- */
2099
- const alAbrir = vestibulo.consumoDeSesion();
2100
- emitir({
2101
- clase: "consumo",
2102
- modelo: alAbrir?.modelo ?? SIN_CONSUMO,
2103
- externo: alAbrir?.externo ?? SIN_CONSUMO,
2104
- ventana: ventanaDeAhora(alAbrir?.contexto ?? 0),
2105
- });
2106
- // Y la cola se vuelve a mirar: este proyecto queda bloqueado para las tareas —gana la
2107
- // persona— y el que estuviera abierto antes acaba de quedar libre.
2108
- opciones.revisarTareas?.();
2233
+ trasAbrirSesion();
2109
2234
  }
2110
2235
  catch (error) {
2111
2236
  aviso = error instanceof Error ? error.message : String(error);
@@ -2843,6 +2968,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2843
2968
  const motivoLegible = (error) => typeof error === "object" && error !== null && "code" in error
2844
2969
  ? codigoDe(error)
2845
2970
  : (error instanceof Error ? error.message : String(error)).split(/\r?\n/)[0].slice(0, 200);
2971
+ /**
2972
+ * `motivoLegible` para el GESTOR de tareas, con los correos TACHADOS: el texto de una tool de
2973
+ * Jira que falla llega tal cual (`servicioDeConectores.ts#ErrorDeTool`) y puede nombrar a una
2974
+ * persona por su correo, que no cruza el cable (IXCODE-11). Se tacha ANTES de recortar: un
2975
+ * correo cortado por el tope queda sin su punto y ya no lo reconoce ningún patrón.
2976
+ */
2977
+ const motivoDelGestor = (error) => typeof error === "object" && error !== null && "code" in error
2978
+ ? codigoDe(error)
2979
+ : sinCorreos((error instanceof Error ? error.message : String(error)).split(/\r?\n/)[0]).slice(0, 200);
2846
2980
  /**
2847
2981
  * El árbol del proyecto abierto. Sin proyecto no hay pestaña que lo pida, así que no se
2848
2982
  * contesta nada; sin PUERTO sí se contesta, con error: un árbol que nunca llega deja al
@@ -2914,6 +3048,507 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
2914
3048
  emitir({ clase: "planes", error: "no se pudieron leer los planes" });
2915
3049
  }
2916
3050
  };
3051
+ /**
3052
+ * El gestor de tareas de cada conector, UNO por conector mientras viva este cable
3053
+ * (IXCODE-11). La fábrica puede devolver una instancia nueva en cada llamada, y el adaptador
3054
+ * de Jira guarda en la instancia la url pública de cada sitio —la que compone el enlace de
3055
+ * cada tarea—, así que pedirla otra vez en cada mensaje perdería esa url. Se memoriza también
3056
+ * el `undefined`: un conector que no es un gestor no deja de serlo entre dos mensajes.
3057
+ */
3058
+ const gestores = new Map();
3059
+ const gestorDe = (conector) => {
3060
+ if (opciones.gestorDeTareas === undefined)
3061
+ return undefined;
3062
+ if (!gestores.has(conector))
3063
+ gestores.set(conector, opciones.gestorDeTareas(conector));
3064
+ return gestores.get(conector);
3065
+ };
3066
+ /**
3067
+ * Las instancias cuyo `sitios()` ya contestó en este proceso, y el nombre de cada sitio.
3068
+ * Sin esa pregunta el adaptador no sabe la url del sitio y las tareas llegarían SIN enlace
3069
+ * —ausente, no inventado—; con ella llegan con el suyo. Se marca solo lo que CONTESTÓ: un
3070
+ * fallo se vuelve a intentar en la acción siguiente.
3071
+ */
3072
+ const conSitios = new WeakSet();
3073
+ const nombreDelSitio = new Map();
3074
+ const preguntarSitios = async (gestor) => {
3075
+ const lista = (await gestor.sitios()).map((x) => ({ id: x.id, nombre: x.nombre }));
3076
+ conSitios.add(gestor);
3077
+ for (const x of lista)
3078
+ nombreDelSitio.set(x.id, x.nombre);
3079
+ return lista;
3080
+ };
3081
+ const asegurarSitios = async (gestor) => {
3082
+ if (!conSitios.has(gestor))
3083
+ await preguntarSitios(gestor);
3084
+ };
3085
+ /**
3086
+ * El nombre para mostrar de cada proyecto que un gestor ha NOMBRADO en este proceso (IXCODE-15):
3087
+ * de `proyectos` (Jira) o de `describir`/`vincular` (Notion, donde el proyecto es un
3088
+ * `collection://…` que no se lee). Lo que no se ha oído no se inventa: el vínculo va sin él.
3089
+ */
3090
+ const nombreDelProyecto = new Map();
3091
+ const claveDeProyecto = (conector, sitio, proyecto) => JSON.stringify([conector, sitio, proyecto]);
3092
+ /**
3093
+ * El nombre del conector tal como lo enseña el catálogo («Jira», «Notion»): los rótulos salen de
3094
+ * aquí y no a fuego (IXCODE-15). Sin servicio, la fila del catálogo de CÓDIGO; sin fila, el id.
3095
+ */
3096
+ const nombreDelConector = (conector) => {
3097
+ let catalogo = CATALOGO_DE_CONECTORES;
3098
+ try {
3099
+ if (servicioConectores !== undefined)
3100
+ catalogo = servicioConectores.lista().catalogo;
3101
+ }
3102
+ catch {
3103
+ // Un fichero de conectores ilegible no puede tumbar un rótulo: queda el catálogo de código.
3104
+ }
3105
+ return catalogo.find((c) => c.id === conector)?.nombre ?? conector;
3106
+ };
3107
+ /** El texto que «Empezar» deja en el compositor. Lo que no consta no se escribe: ni una
3108
+ * línea `undefined` por un enlace que falta, ni un párrafo vacío por una descripción vacía.
3109
+ * La cabecera lleva lo que se ENSEÑA (`etiqueta ?? clave`) y el gestor por su NOMBRE. */
3110
+ const borradorDeTarea = (f, conector) => {
3111
+ const cabecera = [`${f.etiqueta ?? f.clave} — ${f.titulo}`, `Estado: ${f.estado}`, ...(f.url === undefined ? [] : [f.url])].join("\n");
3112
+ const descripcion = f.descripcion.trim();
3113
+ return `Trabaja en esta tarea de ${nombreDelConector(conector)}.\n\n${cabecera}${descripcion === "" ? "" : `\n\n${descripcion}`}`;
3114
+ };
3115
+ /**
3116
+ * El gestor de tareas del proyecto abierto (IXCODE-11, panel del proyecto). Mismas reglas
3117
+ * que `atenderPlanes`: sin proyecto no se contesta; sin gestor en esta ejecución se contesta
3118
+ * con error —un panel en «consultando…» para siempre es un fallo mudo—; y un fallo viaja
3119
+ * sin la ruta de la máquina (`motivoLegible`: nuestra frase, o el código si es de Node).
3120
+ *
3121
+ * La configuración se lee y se escribe en el `config.json` del PROYECTO, nunca fusionada con
3122
+ * la global: el vínculo y los conectores son de esta carpeta.
3123
+ *
3124
+ * **Cuatro acciones ESCRIBEN en Jira (Task 10), y solo esas**: `transicionar` dentro de
3125
+ * `empezar` (con `transicion`) y dentro de `cerrar` (con `transicion`), y `comentar` dentro
3126
+ * de `cerrar`. Las cuatro entran por un mensaje EXPLÍCITO del cliente —la persona aprobó una
3127
+ * tarjeta—, nunca solas: `pendientes`, `transiciones` y `borradorDeCierre` son de solo
3128
+ * LECTURA. `empezar` abre una sesión y propone un borrador; `borradorDeCierre` compone el
3129
+ * texto de cierre de la sesión ABIERTA sin mandarlo.
3130
+ */
3131
+ const atenderGestor = async (m) => {
3132
+ const abierto = vestibulo.proyectoAbierto();
3133
+ if (abierto === undefined)
3134
+ return;
3135
+ const raiz = abierto.raiz;
3136
+ // `sinCorreos` también aquí, y no solo en `motivoDelGestor`: la frase de un fallo puede
3137
+ // llevar un texto del gestor (una clave de proyecto, un nombre de conector) sin pasar por él.
3138
+ // El fallo de `ficha` lleva la CLAVE: con dos filas desplegadas una tras otra, el de la
3139
+ // primera no puede acabar pintado bajo la segunda.
3140
+ // IXCODE-15: el de `buscarProyectos` lleva el TEXTO buscado y el de `describir` la base PEDIDA,
3141
+ // por la misma razón que la clave de `ficha`: el cliente no pinta el fallo de una búsqueda o
3142
+ // una base que ya no tiene delante.
3143
+ const fallo = (motivo) => emitir({
3144
+ clase: "gestor",
3145
+ error: {
3146
+ accion: m.accion,
3147
+ motivo: sinCorreos(motivo),
3148
+ ...(m.accion === "ficha" ? { clave: m.clave } : {}),
3149
+ ...(m.accion === "buscarProyectos" ? { texto: m.texto } : {}),
3150
+ ...(m.accion === "describir" ? { pedido: m.proyecto } : {}),
3151
+ },
3152
+ });
3153
+ if (opciones.gestorDeTareas === undefined)
3154
+ return fallo("esta ejecución no tiene gestor de tareas");
3155
+ const delProyecto = () => {
3156
+ const c = cargar(raiz).config.proyecto;
3157
+ return { conectores: c?.conectores ?? [], vinculo: c?.gestorDeTareas };
3158
+ };
3159
+ /**
3160
+ * El estado del gestor del proyecto, SIN tocar la red (IXCODE-15): lo contestan `vincular`,
3161
+ * `desvincular` y `usarConector`, y un gestor que no responde no puede colgar la casilla de un
3162
+ * conector del chat. `admiteMias` se decidió AL VINCULAR y vive en el `config.json`
3163
+ * (`admiteMiasDelVinculo`: lo guardado, o `true` para un Jira de antes); viaja solo con vínculo.
3164
+ */
3165
+ const emitirEstado = () => {
3166
+ const { conectores, vinculo } = delProyecto();
3167
+ const nombre = vinculo === undefined ? undefined : nombreDelSitio.get(vinculo.sitio);
3168
+ // El nombre del proyecto se GUARDA con el vínculo (IXCODE-15) y sobrevive a un reinicio; lo
3169
+ // oído en este proceso solo sirve de respaldo para un vínculo escrito antes de ese campo.
3170
+ const nombreProyecto = vinculo === undefined
3171
+ ? undefined
3172
+ : (vinculo.nombreDelProyecto ?? nombreDelProyecto.get(claveDeProyecto(vinculo.conector, vinculo.sitio, vinculo.proyecto)));
3173
+ const admiteMias = vinculo === undefined ? undefined : admiteMiasDelVinculo(vinculo);
3174
+ emitir({
3175
+ clase: "gestor",
3176
+ estado: {
3177
+ conectores,
3178
+ ...(vinculo === undefined
3179
+ ? {}
3180
+ : {
3181
+ vinculo: {
3182
+ conector: vinculo.conector,
3183
+ sitio: vinculo.sitio,
3184
+ proyecto: vinculo.proyecto,
3185
+ ...(nombre === undefined ? {} : { nombreDelSitio: nombre }),
3186
+ ...(nombreProyecto === undefined ? {} : { nombreDelProyecto: nombreProyecto }),
3187
+ },
3188
+ }),
3189
+ ...(admiteMias === undefined ? {} : { admiteMias }),
3190
+ },
3191
+ });
3192
+ };
3193
+ /** Escribir el `config.json`: su error puede llevar la ruta, así que al cable va una frase fija. */
3194
+ const guardar = (escribir) => {
3195
+ try {
3196
+ escribir();
3197
+ return true;
3198
+ }
3199
+ catch (error) {
3200
+ informar(`no se pudo guardar la configuración del proyecto (${codigoDe(error)})`);
3201
+ fallo("no se pudo guardar la configuración del proyecto");
3202
+ return false;
3203
+ }
3204
+ };
3205
+ const gestorOFallo = (conector) => {
3206
+ const g = gestorDe(conector);
3207
+ if (g === undefined)
3208
+ fallo(`«${conector}» no es un gestor de tareas de esta ejecución`);
3209
+ return g;
3210
+ };
3211
+ /**
3212
+ * IXCODE-15: una LECTURA que depende del vínculo del proyecto (pendientes, ficha, las
3213
+ * transiciones de «empezar») tarda lo que tarde el gestor, y mientras tanto la persona puede
3214
+ * vincular OTRO. La respuesta es del vínculo de ANTES: se relee el de ahora al volver y, si ya
3215
+ * no es el mismo, la respuesta —o su fallo— se TIRA sin emitir nada. El panel ya pidió lo suyo
3216
+ * al nuevo vínculo; fundirla pintaría filas de Jira bajo la base de Notion. `undefined` = tirada.
3217
+ */
3218
+ const delMismoVinculo = async (usado, leer) => {
3219
+ const sigue = () => {
3220
+ const ahora = delProyecto().vinculo;
3221
+ return ahora !== undefined && ahora.conector === usado.conector && ahora.sitio === usado.sitio && ahora.proyecto === usado.proyecto;
3222
+ };
3223
+ try {
3224
+ const valor = await leer();
3225
+ return sigue() ? { valor } : undefined;
3226
+ }
3227
+ catch (error) {
3228
+ if (!sigue())
3229
+ return undefined;
3230
+ throw error;
3231
+ }
3232
+ };
3233
+ let anunciada = false;
3234
+ try {
3235
+ switch (m.accion) {
3236
+ case "estado":
3237
+ return emitirEstado();
3238
+ case "sitios": {
3239
+ const g = gestorOFallo(m.conector);
3240
+ if (g === undefined)
3241
+ return;
3242
+ emitir({ clase: "gestor", sitios: { conector: m.conector, lista: await preguntarSitios(g) } });
3243
+ return;
3244
+ }
3245
+ case "proyectos": {
3246
+ const g = gestorOFallo(m.conector);
3247
+ if (g === undefined)
3248
+ return;
3249
+ const lista = (await g.proyectos(m.sitio)).map((x) => ({ clave: x.clave, nombre: x.nombre }));
3250
+ for (const x of lista)
3251
+ nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, x.clave), x.nombre);
3252
+ emitir({ clase: "gestor", proyectos: { sitio: m.sitio, lista } });
3253
+ return;
3254
+ }
3255
+ // IXCODE-15, de solo LECTURA: buscar proyectos por texto (Notion: sus bases). Un gestor que
3256
+ // no busca (Jira) lo DICE; una lista vacía afirmaría que no hay nada que se llame así.
3257
+ case "buscarProyectos": {
3258
+ const g = gestorOFallo(m.conector);
3259
+ if (g === undefined)
3260
+ return;
3261
+ if (g.buscarProyectos === undefined)
3262
+ return fallo(`en ${nombreDelConector(m.conector)} los proyectos se eligen de la lista`);
3263
+ const lista = (await g.buscarProyectos(m.texto)).map((x) => ({ proyecto: x.proyecto, nombre: x.nombre, ...(x.ruta === undefined ? {} : { ruta: x.ruta }) }));
3264
+ emitir({ clase: "gestor", busqueda: { conector: m.conector, texto: m.texto, lista } });
3265
+ return;
3266
+ }
3267
+ // IXCODE-15, de solo LECTURA: lo que se entiende del esquema de un proyecto ANTES de
3268
+ // vincularlo. `pedido` es lo que llegó (el id de la base); `esquema.proyecto` es lo que
3269
+ // hay que mandar a `vincular` (el data source ya resuelto). Un «no vale» viaja como
3270
+ // `motivo`, no como `error`: la acción salió bien, y la respuesta es que esa base no sirve.
3271
+ case "describir": {
3272
+ const g = gestorOFallo(m.conector);
3273
+ if (g === undefined)
3274
+ return;
3275
+ if (g.describirProyecto === undefined)
3276
+ return fallo(`${nombreDelConector(m.conector)} no describe sus proyectos`);
3277
+ const [sitio] = await g.sitios();
3278
+ const d = await g.describirProyecto({ conector: m.conector, sitio: sitio?.id ?? "", proyecto: m.proyecto });
3279
+ if ("esquema" in d && sitio !== undefined)
3280
+ nombreDelProyecto.set(claveDeProyecto(m.conector, sitio.id, d.esquema.proyecto), d.esquema.nombre);
3281
+ emitir({ clase: "gestor", descripcion: { conector: m.conector, pedido: m.proyecto, ...d } });
3282
+ return;
3283
+ }
3284
+ case "vincular": {
3285
+ // La forma se comprueba ANTES de preguntar, con la regla de SU conector (IXCODE-15): la
3286
+ // clave de Jira entra en una JQL, el data source de Notion en un SQL.
3287
+ const motivo = motivoDeProyectoInaceptable(m.conector, m.proyecto);
3288
+ if (motivo !== undefined)
3289
+ return fallo(motivo);
3290
+ const g = gestorOFallo(m.conector);
3291
+ if (g === undefined)
3292
+ return;
3293
+ // Solo se escribe lo que el gestor DICE que existe: un vínculo a un proyecto que no se
3294
+ // ve no daría ni una pendiente, y el fallo aparecería lejos de donde se cometió. Quien
3295
+ // sabe DESCRIBIR (Notion) lo comprueba así —que el data source existe Y que su esquema
3296
+ // vale como gestor—; quien no (Jira), contra la lista de proyectos del sitio, como siempre.
3297
+ // `admiteMias` se DECIDE aquí, con la base ya descrita, y se guarda con el vínculo: así
3298
+ // `estado` no pregunta a nadie. Con esquema, solo si tiene una propiedad de persona;
3299
+ // sin describir (Jira), siempre —`currentUser()` no depende del proyecto—.
3300
+ let admiteMias = true;
3301
+ /** El nombre que el gestor DIJO del proyecto: se guarda con el vínculo, para mostrarlo tras un reinicio. */
3302
+ let nombreVisto;
3303
+ if (g.describirProyecto !== undefined) {
3304
+ if (!(await g.sitios()).some((x) => x.id === m.sitio))
3305
+ return fallo(`«${m.sitio}» no es un sitio de ${nombreDelConector(m.conector)}`);
3306
+ const d = await g.describirProyecto({ conector: m.conector, sitio: m.sitio, proyecto: m.proyecto });
3307
+ if ("motivo" in d)
3308
+ return fallo(d.motivo);
3309
+ if (d.esquema.proyecto !== m.proyecto)
3310
+ return fallo("ese proyecto no es el que se describió");
3311
+ nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, m.proyecto), d.esquema.nombre);
3312
+ nombreVisto = d.esquema.nombre;
3313
+ admiteMias = d.esquema.asignado !== undefined;
3314
+ }
3315
+ else {
3316
+ const visibles = await g.proyectos(m.sitio);
3317
+ const visto = visibles.find((x) => x.clave === m.proyecto);
3318
+ if (visto === undefined)
3319
+ return fallo(`«${m.proyecto}» no está entre los proyectos de ese sitio`);
3320
+ nombreDelProyecto.set(claveDeProyecto(m.conector, m.sitio, m.proyecto), visto.nombre);
3321
+ nombreVisto = visto.nombre;
3322
+ }
3323
+ // UN gestor por proyecto (IXCODE-15): vincular uno SUSTITUYE al de antes, y el conector
3324
+ // del de antes deja de estar usado —el panel no tiene casilla para un gestor, así que
3325
+ // quedarse en `conectores` lo dejaría usado sin control que lo quite—.
3326
+ const { conectores, vinculo: anterior } = delProyecto();
3327
+ const vinculo = {
3328
+ conector: m.conector,
3329
+ sitio: m.sitio,
3330
+ proyecto: m.proyecto,
3331
+ admiteMias,
3332
+ ...(nombreVisto.trim() === "" ? {} : { nombreDelProyecto: nombreVisto }),
3333
+ };
3334
+ if (!guardar(() => guardarGestorDeProyecto(raiz, vinculo)))
3335
+ return;
3336
+ const sinAnterior = anterior !== undefined && anterior.conector !== m.conector ? conectores.filter((c) => c !== anterior.conector) : conectores;
3337
+ const nuevos = sinAnterior.includes(m.conector) ? sinAnterior : [...sinAnterior, m.conector];
3338
+ if (nuevos.join("\u0000") !== conectores.join("\u0000") && !guardar(() => guardarConectoresDeProyecto(raiz, nuevos)))
3339
+ return;
3340
+ return emitirEstado();
3341
+ }
3342
+ case "desvincular": {
3343
+ // Desvincular también DEJA DE USAR el conector del vínculo: el panel ya no ofrece una
3344
+ // casilla aparte para un gestor de tareas —vincular lo marca como usado, y esto es la
3345
+ // vuelta—, así que quedarse en `conectores` lo dejaría usado sin control que lo quite.
3346
+ const { conectores, vinculo } = delProyecto();
3347
+ if (!guardar(() => guardarGestorDeProyecto(raiz, undefined)))
3348
+ return;
3349
+ if (vinculo !== undefined && conectores.includes(vinculo.conector)) {
3350
+ if (!guardar(() => guardarConectoresDeProyecto(raiz, conectores.filter((c) => c !== vinculo.conector))))
3351
+ return;
3352
+ }
3353
+ return emitirEstado();
3354
+ }
3355
+ case "usarConector": {
3356
+ // Solo se AÑADE lo que esta consola conoce (`catálogo ∪ definiciones`): un id cualquiera
3357
+ // escrito en el config.json no sería un conector de nadie. QUITAR no se comprueba, para
3358
+ // poder limpiar uno que ya no existe.
3359
+ if (m.usar) {
3360
+ if (servicioConectores === undefined)
3361
+ return fallo("esta ejecución no tiene conectores");
3362
+ if (!servicioConectores.lista().catalogo.some((c) => c.id === m.conector))
3363
+ return fallo(`«${m.conector}» no es un conector de esta consola`);
3364
+ }
3365
+ const { conectores } = delProyecto();
3366
+ const nuevos = m.usar
3367
+ ? (conectores.includes(m.conector) ? conectores : [...conectores, m.conector])
3368
+ : conectores.filter((c) => c !== m.conector);
3369
+ if (!guardar(() => guardarConectoresDeProyecto(raiz, nuevos)))
3370
+ return;
3371
+ return emitirEstado();
3372
+ }
3373
+ case "pendientes": {
3374
+ const { vinculo } = delProyecto();
3375
+ if (vinculo === undefined)
3376
+ return fallo("este proyecto no tiene gestor de tareas");
3377
+ const g = gestorOFallo(vinculo.conector);
3378
+ if (g === undefined)
3379
+ return;
3380
+ const cuando = Date.now();
3381
+ const mias = m.mias === true;
3382
+ const leida = await delMismoVinculo(vinculo, async () => {
3383
+ await asegurarSitios(g);
3384
+ return g.pendientes(vinculo, m.texto, mias ? { mias } : undefined);
3385
+ });
3386
+ if (leida === undefined)
3387
+ return;
3388
+ const lista = leida.valor;
3389
+ emitir({
3390
+ clase: "gestor",
3391
+ pendientes: { cuando, ...(m.texto === undefined ? {} : { texto: m.texto }), ...(mias ? { mias: true } : {}), lista },
3392
+ });
3393
+ return;
3394
+ }
3395
+ // De solo LECTURA: la descripción de UNA tarea, para desplegarla en su fila. Viajan la
3396
+ // clave y el texto y nada más —el resto de la ficha ya está en la fila—.
3397
+ case "ficha": {
3398
+ const { vinculo } = delProyecto();
3399
+ if (vinculo === undefined)
3400
+ return fallo("este proyecto no tiene gestor de tareas");
3401
+ const g = gestorOFallo(vinculo.conector);
3402
+ if (g === undefined)
3403
+ return;
3404
+ const leida = await delMismoVinculo(vinculo, () => g.ficha(vinculo, m.clave));
3405
+ if (leida === undefined)
3406
+ return;
3407
+ const f = leida.valor;
3408
+ emitir({ clase: "gestor", ficha: { clave: f.clave, descripcion: f.descripcion } });
3409
+ return;
3410
+ }
3411
+ // Task 10 (IXCODE-11): la primera de las CUATRO acciones que escriben en Jira, y
3412
+ // solo LEE —la lista de transiciones y cuál se propone—, para las dos tarjetas que
3413
+ // van a pedir la aprobación (una tarea del cliente).
3414
+ case "transiciones": {
3415
+ // Las de CIERRE se piden al conector y sitio del TICKET de la sesión, que es a donde
3416
+ // `cerrar` las aplica: los ids de una transición son de SU sitio, y el vínculo del
3417
+ // proyecto puede apuntar ya a otro —o a ninguno—.
3418
+ const ticket = abierto.ticket;
3419
+ if (m.para === "cerrar" && ticket === undefined)
3420
+ return fallo("esta sesión no está ligada a una tarea");
3421
+ const vinculo = m.para === "cerrar" && ticket !== undefined
3422
+ ? { conector: ticket.conector, sitio: ticket.sitio, proyecto: delProyecto().vinculo?.proyecto ?? "" }
3423
+ : delProyecto().vinculo;
3424
+ if (vinculo === undefined)
3425
+ return fallo("este proyecto no tiene gestor de tareas");
3426
+ const g = gestorOFallo(vinculo.conector);
3427
+ if (g === undefined)
3428
+ return;
3429
+ // Las de CIERRE van con el TICKET, que no cambia al revincular: solo las de «empezar»
3430
+ // (del vínculo del proyecto) se tiran si el vínculo cambió mientras se leían.
3431
+ const leida = m.para === "cerrar" ? { valor: await g.transiciones(vinculo, m.clave) } : await delMismoVinculo(vinculo, () => g.transiciones(vinculo, m.clave));
3432
+ if (leida === undefined)
3433
+ return;
3434
+ const lista = leida.valor;
3435
+ const propuesta = transicionPropuesta(lista, m.para)?.id;
3436
+ emitir({ clase: "gestor", transiciones: { clave: m.clave, para: m.para, lista, ...(propuesta === undefined ? {} : { propuesta }) } });
3437
+ return;
3438
+ }
3439
+ case "empezar": {
3440
+ // Con un turno en vuelo `abrirProyecto` devuelve la MISMA consola (el vestíbulo vuelve a
3441
+ // lo que trabaja): transicionar antes de saberlo movería el ticket en Jira y luego
3442
+ // contestaría «no se pudo abrir una sesión nueva». Se comprueba ANTES de tocar nada.
3443
+ if (abierto.turnoEnVuelo)
3444
+ return fallo("espera a que termine el turno");
3445
+ const { vinculo } = delProyecto();
3446
+ if (vinculo === undefined)
3447
+ return fallo("este proyecto no tiene gestor de tareas");
3448
+ const g = gestorOFallo(vinculo.conector);
3449
+ if (g === undefined)
3450
+ return;
3451
+ await asegurarSitios(g);
3452
+ // La ficha ANTES de abrir nada: si el gestor no contesta, no queda una sesión vacía.
3453
+ const ficha = await g.ficha(vinculo, m.clave);
3454
+ // La transición ANTES de abrir la sesión, y su fallo no aborta el resto: la persona
3455
+ // ya aprobó la tarjeta esperando entrar a trabajar, y quedarse sin sesión por un
3456
+ // «no se pudo transicionar» sería peor que decirlo y abrir igual con el borrador.
3457
+ if (m.transicion !== undefined) {
3458
+ try {
3459
+ await g.transicionar(vinculo, ficha.clave, m.transicion);
3460
+ }
3461
+ catch (error) {
3462
+ fallo(motivoDelGestor(error));
3463
+ }
3464
+ }
3465
+ const hiloDeAntes = abierto.idDeHilo;
3466
+ // Como en `atenderSesion`: un aviso de otra apertura no viaja en el alta de esta.
3467
+ aviso = undefined;
3468
+ // Una sesión NUEVA del mismo proyecto: el camino de `atenderSesion` sin `sesion`.
3469
+ const nueva = await vestibulo.abrirProyecto({ raiz });
3470
+ trasAbrirSesion();
3471
+ // El ticket se estampa solo si de verdad se abrió OTRA: estamparlo en la de antes
3472
+ // ligaría a la tarea una conversación que no se abrió para ella.
3473
+ if (nueva.raiz !== raiz || nueva.idDeHilo === hiloDeAntes)
3474
+ return fallo("no se pudo abrir una sesión nueva");
3475
+ nueva.fijarTicket({ conector: vinculo.conector, sitio: vinculo.sitio, clave: ficha.clave });
3476
+ // El alta ANTES que el borrador: el cliente tiene que estar ya en la sesión nueva
3477
+ // cuando el texto llega al compositor, o el cambio de sesión se lo llevaría.
3478
+ await anunciarAlta().catch(contar);
3479
+ anunciada = true;
3480
+ emitir({ clase: "gestor", borrador: { clave: ficha.clave, texto: borradorDeTarea(ficha, vinculo.conector) } });
3481
+ return;
3482
+ }
3483
+ // Las otras dos acciones que ESCRIBEN (Task 10): de la sesión ABIERTA, no del proyecto
3484
+ // —una sesión puede seguir ligada a un ticket aunque el proyecto se haya desvinculado
3485
+ // o vinculado a otro sitio después—, y solo con el turno ya terminado: escribir sobre
3486
+ // una sesión que sigue trabajando compite con lo que el agente está a punto de dejar.
3487
+ case "borradorDeCierre": {
3488
+ const ticket = abierto.ticket;
3489
+ if (ticket === undefined)
3490
+ return fallo("esta sesión no está ligada a una tarea");
3491
+ if (abierto.turnoEnVuelo)
3492
+ return fallo("espera a que termine el turno");
3493
+ const comentario = comentarioDeCierre(await datosDeCierre(raiz, abierto.idDeHilo));
3494
+ emitir({ clase: "gestor", cierre: { clave: ticket.clave, comentario } });
3495
+ return;
3496
+ }
3497
+ case "cerrar": {
3498
+ const ticket = abierto.ticket;
3499
+ if (ticket === undefined)
3500
+ return fallo("esta sesión no está ligada a una tarea");
3501
+ if (abierto.turnoEnVuelo)
3502
+ return fallo("espera a que termine el turno");
3503
+ if (m.comentario.trim() === "")
3504
+ return fallo("el comentario no puede estar vacío");
3505
+ const g = gestorOFallo(ticket.conector);
3506
+ if (g === undefined)
3507
+ return;
3508
+ // El adaptador de Jira solo mira `sitio` para comentar y transicionar
3509
+ // (`gestorJira.ts#comentar`/`#transicionar`), y el de Notion saca el data source de la
3510
+ // PROPIA página (`gestorNotion.ts#fuenteParaPagina`): el `proyecto` del vínculo del
3511
+ // TICKET no hace falta para escribir, así que basta con lo que la sesión guardó al
3512
+ // abrirse. Se completa con el del proyecto por si acaso; vacío si ya no hay vínculo.
3513
+ const vinculo = { conector: ticket.conector, sitio: ticket.sitio, proyecto: delProyecto().vinculo?.proyecto ?? "" };
3514
+ // Si `comentar` falla, la excepción sale al `catch` de fuera: NO se transiciona.
3515
+ await g.comentar(vinculo, ticket.clave, m.comentario);
3516
+ // El comentario YA está en Jira: si la transición falla, contestar `error{cerrar}`
3517
+ // invitaría a repetir el cierre entero —y a comentar DOS veces—. Sale `cerrado`, sin
3518
+ // `transicion` y con el motivo en `falloDeTransicion`.
3519
+ let falloDeTransicion;
3520
+ if (m.transicion !== undefined) {
3521
+ try {
3522
+ await g.transicionar(vinculo, ticket.clave, m.transicion);
3523
+ }
3524
+ catch (error) {
3525
+ falloDeTransicion = motivoDelGestor(error);
3526
+ }
3527
+ }
3528
+ emitir({
3529
+ clase: "gestor",
3530
+ cerrado: {
3531
+ clave: ticket.clave,
3532
+ comento: true,
3533
+ ...(m.transicion === undefined || falloDeTransicion !== undefined ? {} : { transicion: m.transicion }),
3534
+ ...(falloDeTransicion === undefined ? {} : { falloDeTransicion }),
3535
+ },
3536
+ });
3537
+ return;
3538
+ }
3539
+ }
3540
+ }
3541
+ catch (error) {
3542
+ fallo(motivoDelGestor(error));
3543
+ }
3544
+ finally {
3545
+ // `empezar` pudo cambiar de sesión y fallar después: el alta dice cuál es la de ahora,
3546
+ // igual que al final de `atenderSesion`. Si ya salió antes del borrador, no se repite. Las
3547
+ // demás acciones no mueven el foco, y no se reanuncia nada.
3548
+ if (m.accion === "empezar" && !anunciada)
3549
+ await anunciarAlta().catch(contar);
3550
+ }
3551
+ };
2917
3552
  /**
2918
3553
  * La foto del modelo XOne del proyecto abierto. Mismas reglas que el árbol: sin proyecto no
2919
3554
  * se contesta, sin puerto se contesta con error —un «consultando…» eterno es un fallo mudo—,
@@ -3196,7 +3831,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3196
3831
  // alternativa a inventarse las del primero de la lista antes de que nadie elija.
3197
3832
  proyectoElegido = proyecto;
3198
3833
  // Igual que arriba: el servidor abre por NOMBRE, y el cable trae el id.
3834
+ ramas = [];
3835
+ ramasDe = undefined;
3199
3836
  ramas = await vestibulo.ramasDe(entornoElegido, proyectos.find((p) => p.id === proyecto) ?? proyecto);
3837
+ ramasDe = proyecto;
3200
3838
  return;
3201
3839
  }
3202
3840
  // El proyecto que viene en ESTE mensaje, no el cacheado: lo enviado es la verdad y
@@ -3873,6 +4511,111 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3873
4511
  respuesta.end();
3874
4512
  return;
3875
4513
  }
4514
+ if (typeof mensaje === "object" &&
4515
+ mensaje !== null &&
4516
+ mensaje.clase === "entorno" &&
4517
+ mensaje.accion === "fijados") {
4518
+ // El mismo trato que `visibles`: se guarda con el entorno y el alta siguiente lo trae.
4519
+ void vestibulo
4520
+ .guardarProyectosFijados(mensaje.entorno, mensaje.proyectos)
4521
+ .catch(contar)
4522
+ .finally(() => void anunciarAlta().catch(contar));
4523
+ respuesta.writeHead(204);
4524
+ respuesta.end();
4525
+ return;
4526
+ }
4527
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "resumen") {
4528
+ // La FOTO del resumen del proyecto: lo que solo sabe el servidor, contestado en la
4529
+ // propia respuesta —como el 409 de las negativas—, así que llega solo a quien la pidió.
4530
+ // Del cable llega el ID; el nombre y la raíz los pone el servidor, como en `borrar`.
4531
+ const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
4532
+ if (entornoElegido === undefined || identidad === undefined) {
4533
+ respuesta.writeHead(409, { "content-type": "application/json" });
4534
+ respuesta.end(JSON.stringify({ motivo: "ese proyecto no está en el listado del entorno activo" }));
4535
+ return;
4536
+ }
4537
+ const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
4538
+ // Las tareas de ESTE proyecto las decide la RAÍZ y no el id: una tarea no lleva su
4539
+ // entorno, y el mismo id en otro entorno —o un clon del mismo servidor con otro id—
4540
+ // se colaría filtrando en el cliente. Viajan los ids; sus datos vivos ya están allí.
4541
+ const tareas = (opciones.colaDeTareas?.listar() ?? [])
4542
+ .filter((t) => mismaRuta(t.proyecto.raiz, raiz))
4543
+ .map((t) => t.id);
4544
+ let sync;
4545
+ if (esProyectoEnDisco(raiz)) {
4546
+ // La MISMA lectura que la banda de CloudStudio en Revisión, y no una segunda cuenta:
4547
+ // dos pantallas midiendo lo mismo de dos formas acaban diciendo dos cifras.
4548
+ const { clase: _clase, ...medida } = await lecturaDeSync(raiz);
4549
+ sync = medida;
4550
+ }
4551
+ const foto = { tareas, ...(sync === undefined ? {} : { sync }) };
4552
+ respuesta.writeHead(200, { "content-type": "application/json" });
4553
+ respuesta.end(JSON.stringify(foto));
4554
+ return;
4555
+ }
4556
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "abrirCarpeta") {
4557
+ // Como `borrar`: del cable llega el ID, el nombre sale del listado del servidor y la
4558
+ // ruta la compone el vestíbulo. Solo con la copia BAJADA: una carpeta a medias de un alta
4559
+ // que falló no es «los ficheros del proyecto».
4560
+ const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
4561
+ const negar = (motivo) => {
4562
+ respuesta.writeHead(409, { "content-type": "application/json" });
4563
+ respuesta.end(JSON.stringify({ motivo }));
4564
+ };
4565
+ if (entornoElegido === undefined || identidad === undefined) {
4566
+ negar("ese proyecto no está en el listado del entorno activo");
4567
+ return;
4568
+ }
4569
+ if (opciones.abrirCarpetaDeProyecto === undefined) {
4570
+ negar("esta ejecución no puede abrir carpetas del sistema");
4571
+ return;
4572
+ }
4573
+ const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
4574
+ if (!esProyectoEnDisco(raiz)) {
4575
+ negar("ese proyecto no está descargado en este equipo");
4576
+ return;
4577
+ }
4578
+ opciones.abrirCarpetaDeProyecto(raiz);
4579
+ respuesta.writeHead(204);
4580
+ respuesta.end();
4581
+ return;
4582
+ }
4583
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "copiaLocal" && mensaje.accion === "borrar") {
4584
+ // Como `olvidar`: la negativa se decide AQUÍ y se ESPERA, para poder devolver el motivo
4585
+ // con un 409 — `informar` no llega al navegador desde el vestíbulo. El nombre sale del
4586
+ // listado del SERVIDOR para ese id: del cliente no se acepta ni un nombre ni una ruta.
4587
+ const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
4588
+ if (entornoElegido === undefined || identidad === undefined) {
4589
+ respuesta.writeHead(409, { "content-type": "application/json" });
4590
+ respuesta.end(JSON.stringify({ motivo: "ese proyecto no está en el listado del entorno activo" }));
4591
+ return;
4592
+ }
4593
+ let resultado;
4594
+ try {
4595
+ resultado = await vestibulo.borrarCopia(entornoElegido, identidad.nombre, (opciones.colaDeTareas?.listar() ?? []).map((t) => ({ estado: t.estado, raiz: t.proyecto.raiz })));
4596
+ }
4597
+ catch (error) {
4598
+ contar(error);
4599
+ resultado = { cerroLaAbierta: false, motivo: "no se pudo borrar la copia local" };
4600
+ }
4601
+ // Lo mismo que al borrar la sesión abierta: el cable se quedaría enganchado a una
4602
+ // consola muerta, así que se muda al vestíbulo —el cliente pinta el escritorio—, y la
4603
+ // raíz queda LIBRE, así que la cola de tareas se vuelve a mirar.
4604
+ if (resultado.cerroLaAbierta) {
4605
+ adjuntar();
4606
+ opciones.revisarTareas?.();
4607
+ }
4608
+ // El alta siguiente ya trae `local` falso, y sin `proyectoActivo` si era el del foco.
4609
+ await anunciarAlta().catch(contar);
4610
+ if (resultado.motivo !== undefined) {
4611
+ respuesta.writeHead(409, { "content-type": "application/json" });
4612
+ respuesta.end(JSON.stringify({ motivo: resultado.motivo }));
4613
+ return;
4614
+ }
4615
+ respuesta.writeHead(204);
4616
+ respuesta.end();
4617
+ return;
4618
+ }
3876
4619
  if (typeof mensaje === "object" &&
3877
4620
  mensaje !== null &&
3878
4621
  mensaje.clase === "entorno" &&
@@ -3968,6 +4711,22 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3968
4711
  respuesta.end();
3969
4712
  return;
3970
4713
  }
4714
+ if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "gestor") {
4715
+ // Los campos se comprueban por ACCIÓN, como `modeloDelCambio`: un mensaje mal formado
4716
+ // se contesta con su error, no revienta dentro de `atenderGestor`.
4717
+ const bien = gestorBienFormado(mensaje);
4718
+ if (bien === undefined) {
4719
+ const accion = typeof mensaje.accion === "string" ? mensaje.accion : "desconocida";
4720
+ if (vestibulo.proyectoAbierto() !== undefined)
4721
+ emitir({ clase: "gestor", error: { accion, motivo: "petición mal formada" } });
4722
+ }
4723
+ else {
4724
+ void atenderGestor(bien).catch(contar);
4725
+ }
4726
+ respuesta.writeHead(204);
4727
+ respuesta.end();
4728
+ return;
4729
+ }
3971
4730
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "colecciones") {
3972
4731
  void atenderColecciones().catch(contar);
3973
4732
  respuesta.writeHead(204);
@@ -4866,6 +5625,48 @@ export function ajusteDeDepuracionCableado(opciones = {}) {
4866
5625
  export function ajusteDeConectoresCableado(opciones) {
4867
5626
  return { conectores: (alCambiar) => servicioDeConectoresCableado({ casa: opciones.casa, alCambiar }) };
4868
5627
  }
5628
+ /**
5629
+ * Las opciones `conectores` y `gestorDeTareas` de `montarRutas`, cableadas JUNTAS (IXCODE-11).
5630
+ *
5631
+ * Juntas porque el gestor de Jira llama por el MISMO servicio de conectores que `montarRutas`
5632
+ * construye con la fábrica `conectores` —el que lleva en memoria las autorizaciones
5633
+ * pendientes: un segundo servicio no vería la de Ajustes y pisaría su verificador PKCE—. Así
5634
+ * que aquí se ENVUELVE esa fábrica para quedarse con el servicio que devuelve, y el gestor le
5635
+ * llama a ese. El gestor de `"jira"` es UNO (el adaptador guarda la url de cada sitio en la
5636
+ * instancia), y el de `"notion"` también (guarda el esquema de cada base, IXCODE-15); cualquier
5637
+ * otro conector no es un gestor. Extraída y probada por el patrón de
5638
+ * fallo de siempre: un literal dentro de `arrancarConsolaWeb` no lo mira ningún test.
5639
+ */
5640
+ export function ajusteDeGestorCableado(ajuste) {
5641
+ let servicio;
5642
+ /** Uno por conector: el de Jira guarda la url de cada sitio, el de Notion el esquema de cada base. */
5643
+ const gestores = new Map();
5644
+ /** Las fábricas de los gestores conocidos (IXCODE-15: Jira y Notion). Cualquier otro conector no es un gestor. */
5645
+ const FABRICAS = {
5646
+ jira: crearGestorJira,
5647
+ notion: crearGestorNotion,
5648
+ };
5649
+ return {
5650
+ conectores: (alCambiar) => (servicio = ajuste.conectores(alCambiar)),
5651
+ gestorDeTareas: (conector) => {
5652
+ const fabrica = Object.hasOwn(FABRICAS, conector) ? FABRICAS[conector] : undefined;
5653
+ if (fabrica === undefined)
5654
+ return undefined;
5655
+ let g = gestores.get(conector);
5656
+ if (g === undefined) {
5657
+ g = fabrica((nombre, args) => {
5658
+ // `montarRutas` construye el servicio al montar, antes de atender ningún mensaje; esto
5659
+ // solo salta si alguien usa el gestor sin haber pasado `conectores` a `montarRutas`.
5660
+ if (servicio === undefined)
5661
+ return Promise.reject(new Error("esta ejecución no tiene conectores"));
5662
+ return servicio.llamar(conector, nombre, args);
5663
+ });
5664
+ gestores.set(conector, g);
5665
+ }
5666
+ return g;
5667
+ },
5668
+ };
5669
+ }
4869
5670
  export function construirCorredorDeTareasCableado(opciones) {
4870
5671
  const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
4871
5672
  /**
@@ -5185,7 +5986,9 @@ export async function arrancarConsolaWeb(opciones) {
5185
5986
  ...ajusteDeDepuracionCableado(),
5186
5987
  // Los conectores MCP, con la MISMA disciplina: la composición extraída y probada
5187
5988
  // (`ajusteDeConectoresCableado`), nunca un literal aquí dentro.
5188
- ...ajusteDeConectoresCableado({ casa: homedir() }),
5989
+ // Y el gestor de tareas (IXCODE-11) llama por ESE mismo servicio: los dos salen de
5990
+ // `ajusteDeGestorCableado`, que envuelve la fábrica de conectores.
5991
+ ...ajusteDeGestorCableado(ajusteDeConectoresCableado({ casa: homedir() })),
5189
5992
  // El selector nativo, solo si este sistema tiene uno. En el que no, la opción no se
5190
5993
  // monta y el botón no llega a existir.
5191
5994
  ...(haySelectorDeCarpeta()
@@ -5247,6 +6050,7 @@ export async function arrancarConsolaWeb(opciones) {
5247
6050
  guardarAjustesDeDispositivos: (ajustes) => void guardarDispositivos(undefined, ajustes),
5248
6051
  instalarHerramienta: instalarHerramientaDeDispositivos,
5249
6052
  abrirCarpetaDeHerramienta: abrirCarpetaDelSistema,
6053
+ abrirCarpetaDeProyecto: abrirDirectorioDelSistema,
5250
6054
  verificarDispositivo,
5251
6055
  // Con la función REAL, y no declarada y sin pasar: es la trampa que este repo ha pagado
5252
6056
  // nueve veces, y la que el comentario de justo abajo describe para las cuatro del