xonecode 0.38.15 → 0.38.20

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 (61) hide show
  1. package/apps/web/dist/assets/{EditorDeFichero-C9BEoDWh.js → EditorDeFichero-DySw4j79.js} +1 -1
  2. package/apps/web/dist/assets/index-D7OcbgXE.css +1 -0
  3. package/apps/web/dist/assets/index-vvfKjRva.js +506 -0
  4. package/apps/web/dist/assets/{php-DIdt3Vgn.js → php-CSyDZ6m1.js} +1 -1
  5. package/apps/web/dist/assets/{ruby-DYEvg0Dr.js → ruby-3bX3v6o_.js} +1 -1
  6. package/apps/web/dist/index.html +2 -2
  7. package/dist/agent/conectores/gestorJira.js +118 -1
  8. package/dist/agent/conectores/gestorJira.js.map +1 -1
  9. package/dist/agent/dispositivos/decodificarImagen.js +8 -0
  10. package/dist/agent/dispositivos/decodificarImagen.js.map +1 -1
  11. package/dist/agent/motores/trueforge/convertirAP.js +86 -0
  12. package/dist/agent/motores/trueforge/convertirAP.js.map +1 -0
  13. package/dist/agent/motores/trueforge/sesionTrueforge.js +37 -2
  14. package/dist/agent/motores/trueforge/sesionTrueforge.js.map +1 -1
  15. package/dist/agent/subagentes/agentesEnDisco.js +28 -3
  16. package/dist/agent/subagentes/agentesEnDisco.js.map +1 -1
  17. package/dist/agent/subagentes/escrituraExterna.js +8 -1
  18. package/dist/agent/subagentes/escrituraExterna.js.map +1 -1
  19. package/dist/agent/subagentes/pluginDeSkills.js +21 -6
  20. package/dist/agent/subagentes/pluginDeSkills.js.map +1 -1
  21. package/dist/agent/subagentes/subagenteExterno.js +8 -3
  22. package/dist/agent/subagentes/subagenteExterno.js.map +1 -1
  23. package/dist/agent/tareas/reflejoEnElGestor.js +120 -0
  24. package/dist/agent/tareas/reflejoEnElGestor.js.map +1 -0
  25. package/dist/agent/tareas/sondeoDelGestor.js +201 -0
  26. package/dist/agent/tareas/sondeoDelGestor.js.map +1 -0
  27. package/dist/agent/turno/turnoReal.js +1 -0
  28. package/dist/agent/turno/turnoReal.js.map +1 -1
  29. package/dist/cli/consola.js.map +1 -1
  30. package/dist/cli/main.js +2 -0
  31. package/dist/cli/main.js.map +1 -1
  32. package/dist/core/agentes.js +34 -0
  33. package/dist/core/agentes.js.map +1 -1
  34. package/dist/core/config.js +29 -0
  35. package/dist/core/config.js.map +1 -1
  36. package/dist/core/convertirAP.js +87 -0
  37. package/dist/core/convertirAP.js.map +1 -0
  38. package/dist/core/entrega.js +23 -0
  39. package/dist/core/entrega.js.map +1 -1
  40. package/dist/core/geometriaDePantalla.js +33 -0
  41. package/dist/core/geometriaDePantalla.js.map +1 -1
  42. package/dist/core/gestorDeTareas.js.map +1 -1
  43. package/dist/core/notify.js +2 -0
  44. package/dist/core/notify.js.map +1 -1
  45. package/dist/core/ports.js +36 -2
  46. package/dist/core/ports.js.map +1 -1
  47. package/dist/core/tareas.js.map +1 -1
  48. package/dist/core/tareasEnElGestor.js +197 -0
  49. package/dist/core/tareasEnElGestor.js.map +1 -0
  50. package/dist/web/servidor/arranque.js +446 -42
  51. package/dist/web/servidor/arranque.js.map +1 -1
  52. package/dist/web/servidor/consolaDeTarea.js +2 -0
  53. package/dist/web/servidor/consolaDeTarea.js.map +1 -1
  54. package/dist/web/servidor/corredorDeTareas.js +14 -2
  55. package/dist/web/servidor/corredorDeTareas.js.map +1 -1
  56. package/dist/web/servidor/transporte.js +10 -0
  57. package/dist/web/servidor/transporte.js.map +1 -1
  58. package/package.json +1 -1
  59. package/skills/xone-development/references/css/selectores-unidades-colores.md +2 -0
  60. package/apps/web/dist/assets/index-BpDt_1vN.js +0 -506
  61. package/apps/web/dist/assets/index-D_Z-V9n1.css +0 -1
@@ -28,14 +28,14 @@
28
28
  * en la omisión (Ollama local) con una clave de Anthropic recién escrita al lado.
29
29
  */
30
30
  import { hayMemoriaDeHilo, olvidarMemoriaDeHilo } from "../../agent/sesiones/memoriaDeHilo.js";
31
- import { existsSync, readFileSync } from "node:fs";
31
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
32
32
  import { lineaDeVersion } from "../../core/version.js";
33
33
  import { esperarInterrupcion } from "./interrupcion.js";
34
34
  import { basename, dirname, join } from "node:path";
35
35
  import { fileURLToPath } from "node:url";
36
36
  import { randomUUID } from "node:crypto";
37
37
  import { homedir } from "node:os";
38
- import { escribirAgente, leerAgente, motivoDeNombreInaceptable } from "../../core/agentes.js";
38
+ import { conLoQueLaVentanaNoEdita, escribirAgente, leerAgente, motivoDeNombreInaceptable } from "../../core/agentes.js";
39
39
  import { motivoDeNombreDeSkillInaceptable, nombreDeSkillSugerido, } from "../../core/skills.js";
40
40
  import { borrarSkill, cargarSkills, esDeSerie as esDeSerieLaSkill, guardarSkill, instalarSkillDesdeZip, } from "../../agent/grafo/skills.js";
41
41
  import { TOPE_DEL_ZIP } from "../../core/zipDeSkill.js";
@@ -125,6 +125,9 @@ import { servicioDeConectoresCableado } from "../../agent/conectores/servicioDeC
125
125
  import { crearGestorJira } from "../../agent/conectores/gestorJira.js";
126
126
  import { crearGestorNotion } from "../../agent/conectores/gestorNotion.js";
127
127
  import { admiteMiasDelVinculo, motivoDeProyectoInaceptable, transicionPropuesta, comentarioDeCierre, } from "../../core/gestorDeTareas.js";
128
+ import { cerrarTicket, comentarioDeEncargo, encargoConTicket, esEstado, mapeoPropuesto } from "../../core/tareasEnElGestor.js";
129
+ import { crearSondeoDelGestor } from "../../agent/tareas/sondeoDelGestor.js";
130
+ import { anotarAvisoEnElDisco, conReflejoAlGuardar, crearReflejoEnElGestor } from "../../agent/tareas/reflejoEnElGestor.js";
128
131
  import { datosDeCierre } from "./datosDeCierre.js";
129
132
  import { ErrorDeDescargaCortada, escribirPaquete, listarSoporte, motivoParaNoExportar } from "./soporte.js";
130
133
  import { nombreDelPaquete } from "../../core/paqueteDeSoporte.js";
@@ -242,6 +245,65 @@ function escaparHtml(texto) {
242
245
  export function sinCorreos(texto) {
243
246
  return texto.replace(/[^\s@<>()"'«»]+@[^\s@<>()"'«»]+\.[^\s@<>()"'«»]+/g, "[correo]");
244
247
  }
248
+ /** Un error de un gestor de tareas como texto que puede cruzar el cable y quedarse en una tarea: de Node solo el
249
+ * `code`, y del resto la primera línea con los correos TACHADOS y recortada (la regla de `motivoDelGestor`). */
250
+ export function motivoDeGestorParaElCable(error) {
251
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
252
+ ? error.code
253
+ : sinCorreos((error instanceof Error ? error.message : String(error)).split(/\r?\n/)[0]).slice(0, 200);
254
+ }
255
+ /**
256
+ * Lo que lleva el comentario de cierre de una tarea: lo mismo que el «Cerrar en …» de un chat sobre SU conversación
257
+ * (ficheros y commits de la sesión, el último veredicto del verificador, la última respuesta). Sin conversación, o si
258
+ * no se puede leer, un cierre sin datos —«no corrió», nunca un «verde» inventado—.
259
+ */
260
+ /**
261
+ * Las copias locales con el DISPARO del gestor encendido (la puerta 2): `<workspace>/<entorno>/<proyecto>` con su
262
+ * `config.json`, de donde salen su identidad (`cloudstudio.proyecto`: el id con el que la pestaña Tareas filtra) y su
263
+ * vínculo. Se relee en cada pasada: el interruptor se cambia con todo en marcha. Lo que no se puede leer se salta.
264
+ */
265
+ export function copiasConGestor(base) {
266
+ const copias = [];
267
+ const carpetas = (ruta) => {
268
+ try {
269
+ return readdirSync(ruta, { withFileTypes: true })
270
+ .filter((e) => e.isDirectory() && !e.name.startsWith("."))
271
+ .map((e) => e.name);
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ };
277
+ for (const entorno of carpetas(base)) {
278
+ for (const nombre of carpetas(join(base, entorno))) {
279
+ const raiz = join(base, entorno, nombre);
280
+ if (!existsSync(join(raiz, ".xonecode", "config.json")))
281
+ continue;
282
+ try {
283
+ const c = cargar(raiz).config.proyecto;
284
+ const vinculo = c?.gestorDeTareas;
285
+ const identidad = c?.cloudstudio?.proyecto;
286
+ if (vinculo?.automatico !== true || identidad === undefined)
287
+ continue;
288
+ copias.push({ proyecto: { id: identidad.id, raiz, nombre: identidad.nombre }, vinculo });
289
+ }
290
+ catch {
291
+ // Un config.json roto no para a los demás proyectos.
292
+ }
293
+ }
294
+ }
295
+ return copias;
296
+ }
297
+ export async function datosDeCierreDeTarea(tarea) {
298
+ if (tarea.sesion === undefined)
299
+ return { ficheros: [], commits: [] };
300
+ try {
301
+ return await datosDeCierre(tarea.proyecto.raiz, tarea.sesion);
302
+ }
303
+ catch {
304
+ return { ficheros: [], commits: [] };
305
+ }
306
+ }
245
307
  /**
246
308
  * El mensaje `gestor` con los campos que su ACCIÓN pide, o `undefined`. Llega del navegador sin
247
309
  * tipos: `atenderGestor` confía en esta forma, así que se comprueba aquí y no dentro.
@@ -962,11 +1024,17 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
962
1024
  // mismo fichero que se puede editar a mano, así que el cargador es la única
963
1025
  // autoridad sobre si vale. Si no pasara, el agente desaparecería al siguiente
964
1026
  // arranque sin que nadie hubiera hecho nada raro.
965
- const candidato = {
1027
+ /**
1028
+ * Los campos que la ventana no edita (`escribeEn`, `llama`, `espera`, `pensamiento`) no viajan por el cable: se
1029
+ * toman del agente que la persona tenía DELANTE —el en vigor con ese nombre, o el de antes en un renombrado—, o
1030
+ * el guardado los borraría (`core/agentes.ts#conLoQueLaVentanaNoEdita`).
1031
+ */
1032
+ const previo = cargarAgentes(abierto?.raiz).agentes.find((a) => a.nombre === (renombrandoDe ?? mensaje.agente.nombre));
1033
+ const candidato = conLoQueLaVentanaNoEdita(previo, {
966
1034
  ...mensaje.agente,
967
1035
  motor: mensaje.agente.motor,
968
1036
  origen: mensaje.ambito,
969
- };
1037
+ });
970
1038
  const comprobado = leerAgente(candidato.nombre, escribirAgente(candidato), mensaje.ambito);
971
1039
  if ("error" in comprobado) {
972
1040
  informar(`no se guarda «${candidato.nombre}»: ${comprobado.error}`);
@@ -1640,7 +1708,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1640
1708
  * que la propia ruta de subida aplica: forma de segmento llano, y que no sea ya una
1641
1709
  * tarea.
1642
1710
  */
1643
- borrador) => {
1711
+ borrador,
1712
+ /** El ticket del gestor YA creado (`atenderCrearTareaEnGestor`): se copia a la tarea antes de guardarla, porque
1713
+ * guardar dispara al corredor y el reflejo de «empezar» tiene que encontrarlo. */
1714
+ ticket) => {
1644
1715
  if (opciones.colaDeTareas === undefined)
1645
1716
  return undefined;
1646
1717
  const proyecto = proyectoParaTarea(proyectoId);
@@ -1668,10 +1739,88 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
1668
1739
  adjuntos: opciones.colaDeTareas.listarAdjuntos(id),
1669
1740
  estado: "nuevo",
1670
1741
  creada: new Date().toISOString(),
1742
+ ...(ticket === undefined ? {} : { ticket }),
1671
1743
  };
1672
1744
  opciones.colaDeTareas.guardar([...lista, nueva]);
1673
1745
  return { id: nueva.id };
1674
1746
  };
1747
+ /**
1748
+ * El gestor en el que vivirá una tarea NUEVA de ese proyecto, si lo tiene: su vínculo (del `config.json` del PROYECTO,
1749
+ * leído aquí y no dicho por el cliente: la ventana se abre también desde el escritorio para otro proyecto) y un
1750
+ * gestor que sabe CREAR. Sin vínculo, o con un gestor sin `crear`, `undefined` y la tarea se encola en local como
1751
+ * siempre (límite declarado: lo dice la ventana).
1752
+ */
1753
+ const gestorParaTareaNueva = (proyectoId) => {
1754
+ const proyecto = proyectoParaTarea(proyectoId);
1755
+ if (proyecto === undefined)
1756
+ return undefined;
1757
+ let vinculo;
1758
+ try {
1759
+ vinculo = cargar(proyecto.raiz).config.proyecto?.gestorDeTareas;
1760
+ }
1761
+ catch {
1762
+ return undefined;
1763
+ }
1764
+ if (vinculo === undefined)
1765
+ return undefined;
1766
+ const gestor = gestorDe(vinculo.conector);
1767
+ if (gestor?.crear === undefined)
1768
+ return undefined;
1769
+ return { proyecto, vinculo, gestor: gestor };
1770
+ };
1771
+ /**
1772
+ * Crear una tarea EN el gestor (`docs/superpowers/specs/2026-10-15-tareas-desde-el-gestor-design.md`, puerta 1): el
1773
+ * ticket primero, luego el encargo como comentario, y SOLO entonces la tarea local con el ticket puesto. Si el gestor
1774
+ * no deja crear, NO se crea la tarea local —con vínculo, una tarea que no está en el gestor es justo lo que se
1775
+ * pierde— y se contesta 409 con el motivo. Si el ticket se creó pero el comentario no, la tarea se crea igual y se
1776
+ * dice: el ticket ya existe.
1777
+ */
1778
+ const atenderCrearTareaEnGestor = async (destino, peticion, encargo, borrador) => {
1779
+ if (opciones.colaDeTareas === undefined)
1780
+ return { estado: 409, cuerpo: { motivo: "esta ejecución no tiene cola de tareas" } };
1781
+ // El borrador se comprueba ANTES de tocar el gestor: un ticket creado para una tarea que luego no se guarda quedaría huérfano.
1782
+ if (borrador !== undefined && !esBorradorLibre(borrador, opciones.colaDeTareas.listar())) {
1783
+ return { estado: 409, cuerpo: { motivo: "ese borrador de adjuntos no vale (o ya es una tarea)" } };
1784
+ }
1785
+ const { gestor, vinculo } = destino;
1786
+ // Solo para que el ticket llegue con su enlace: sin la url del sitio se crea igual, sin `url`.
1787
+ await asegurarSitios(gestor).catch(() => { });
1788
+ let creada;
1789
+ try {
1790
+ creada = await gestor.crear(vinculo, { titulo: tituloDeTarea(peticion), descripcion: peticion });
1791
+ }
1792
+ catch (error) {
1793
+ return { estado: 409, cuerpo: { motivo: `no se pudo crear la tarea en el gestor: ${motivoDelGestor(error)}` } };
1794
+ }
1795
+ let aviso;
1796
+ try {
1797
+ await gestor.comentar(vinculo, creada.clave, comentarioDeEncargo(encargo));
1798
+ }
1799
+ catch (error) {
1800
+ aviso = `se creó ${creada.clave}, pero no se pudo subir el encargo: ${motivoDelGestor(error)}`;
1801
+ }
1802
+ const ticket = {
1803
+ conector: vinculo.conector,
1804
+ sitio: vinculo.sitio,
1805
+ proyecto: vinculo.proyecto,
1806
+ clave: creada.clave,
1807
+ ...(creada.url === undefined ? {} : { url: creada.url }),
1808
+ ...(aviso === undefined ? {} : { aviso }),
1809
+ // El mapeo de estados del vínculo, COPIADO: a qué estado pasa al empezar y al terminar.
1810
+ ...(vinculo.estados === undefined ? {} : { estados: vinculo.estados }),
1811
+ };
1812
+ // Con la línea del ticket delante (`encargoConTicket`): en Jira el comentario lleva el encargo SIN ella —ya está en el ticket—.
1813
+ const nueva = atenderCrearTarea(destino.proyecto.id, peticion, encargoConTicket(encargo, ticket), borrador, ticket);
1814
+ if (nueva === undefined) {
1815
+ return { estado: 409, cuerpo: { motivo: `se creó ${creada.clave} en el gestor, pero no la tarea local: créala de nuevo o bórrala allí` } };
1816
+ }
1817
+ opciones.revisarTareas?.();
1818
+ emitirTareas();
1819
+ return {
1820
+ estado: 200,
1821
+ cuerpo: { id: nueva.id, ticket: { clave: creada.clave, ...(creada.url === undefined ? {} : { url: creada.url }) }, ...(aviso === undefined ? {} : { aviso }) },
1822
+ };
1823
+ };
1675
1824
  /**
1676
1825
  * ¿Se puede adoptar ese id de borrador? Segmento llano Y que no sea ya una tarea.
1677
1826
  *
@@ -3671,6 +3820,9 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3671
3820
  proyecto: vinculo.proyecto,
3672
3821
  ...(nombre === undefined ? {} : { nombreDelSitio: nombre }),
3673
3822
  ...(nombreProyecto === undefined ? {} : { nombreDelProyecto: nombreProyecto }),
3823
+ ...(vinculo.estados === undefined ? {} : { estados: vinculo.estados }),
3824
+ ...(vinculo.automatico === true ? { automatico: true } : {}),
3825
+ ...(vinculo.revisarEncargo === true ? { revisarEncargo: true } : {}),
3674
3826
  },
3675
3827
  }),
3676
3828
  ...(admiteMias === undefined ? {} : { admiteMias }),
@@ -3817,6 +3969,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3817
3969
  proyecto: m.proyecto,
3818
3970
  admiteMias,
3819
3971
  ...(nombreVisto.trim() === "" ? {} : { nombreDelProyecto: nombreVisto }),
3972
+ // Volver a vincular el MISMO proyecto del gestor conserva su mapeo de estados y su disparo; otro proyecto
3973
+ // tiene otro flujo, y empieza sin ellos (apagado).
3974
+ ...(anterior !== undefined && anterior.conector === m.conector && anterior.sitio === m.sitio && anterior.proyecto === m.proyecto
3975
+ ? {
3976
+ ...(anterior.estados === undefined ? {} : { estados: anterior.estados }),
3977
+ ...(anterior.automatico === true ? { automatico: true } : {}),
3978
+ ...(anterior.revisarEncargo === true ? { revisarEncargo: true } : {}),
3979
+ }
3980
+ : {}),
3820
3981
  };
3821
3982
  if (!guardar(() => guardarGestorDeProyecto(raiz, vinculo)))
3822
3983
  return;
@@ -3998,20 +4159,10 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
3998
4159
  // TICKET no hace falta para escribir, así que basta con lo que la sesión guardó al
3999
4160
  // abrirse. Se completa con el del proyecto por si acaso; vacío si ya no hay vínculo.
4000
4161
  const vinculo = { conector: ticket.conector, sitio: ticket.sitio, proyecto: delProyecto().vinculo?.proyecto ?? "" };
4001
- // Si `comentar` falla, la excepción sale al `catch` de fuera: NO se transiciona.
4002
- await g.comentar(vinculo, ticket.clave, m.comentario);
4003
- // El comentario YA está en Jira: si la transición falla, contestar `error{cerrar}`
4004
- // invitaría a repetir el cierre entero —y a comentar DOS veces—. Sale `cerrado`, sin
4005
- // `transicion` y con el motivo en `falloDeTransicion`.
4006
- let falloDeTransicion;
4007
- if (m.transicion !== undefined) {
4008
- try {
4009
- await g.transicionar(vinculo, ticket.clave, m.transicion);
4010
- }
4011
- catch (error) {
4012
- falloDeTransicion = motivoDelGestor(error);
4013
- }
4014
- }
4162
+ // La secuencia de `core/tareasEnElGestor.ts#cerrarTicket`, la MISMA que cierra una tarea en background: si
4163
+ // `comentar` falla, sale al `catch` de fuera y NO se transiciona; si falla la transición, el comentario YA está
4164
+ // y sale `cerrado` sin `transicion` y con el motivo (contestar `error{cerrar}` invitaría a comentar DOS veces).
4165
+ const { falloDeTransicion } = await cerrarTicket(g, vinculo, ticket.clave, m.comentario, m.transicion, motivoDelGestor);
4015
4166
  emitir({
4016
4167
  clase: "gestor",
4017
4168
  cerrado: {
@@ -5049,14 +5200,34 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
5049
5200
  responder(403, "ese nombre no es de un artefacto");
5050
5201
  return;
5051
5202
  }
5052
- const abierto = vestibulo.proyectoAbierto();
5053
- if (abierto === undefined || opciones.leerArtefactoCrudo === undefined) {
5203
+ // Sin `p` y `s`, el de la conversación ABIERTA. Con ellos, el de ESA conversación, que se está LEYENDO sin
5204
+ // consola (una tarea finalizada, IXCODE-36): resuelta por el servidor como `leerSesion`, nunca por el texto.
5205
+ let origen;
5206
+ const pedidoProyecto = query.get("p");
5207
+ const pedidaSesion = query.get("s");
5208
+ if (pedidoProyecto !== null || pedidaSesion !== null) {
5209
+ const resuelta = sesionDelDisco(pedidoProyecto, pedidaSesion);
5210
+ if ("motivo" in resuelta) {
5211
+ responder(404, "no hay artefacto");
5212
+ return;
5213
+ }
5214
+ origen = resuelta;
5215
+ }
5216
+ else {
5217
+ const abierto = vestibulo.proyectoAbierto();
5218
+ if (abierto === undefined) {
5219
+ responder(404, "no hay artefacto");
5220
+ return;
5221
+ }
5222
+ origen = { raiz: abierto.raiz, sesion: abierto.idDeHilo };
5223
+ }
5224
+ if (opciones.leerArtefactoCrudo === undefined) {
5054
5225
  responder(404, "no hay artefacto");
5055
5226
  return;
5056
5227
  }
5057
5228
  let leido;
5058
5229
  try {
5059
- leido = await opciones.leerArtefactoCrudo(abierto.raiz, abierto.idDeHilo, nombre);
5230
+ leido = await opciones.leerArtefactoCrudo(origen.raiz, origen.sesion, nombre);
5060
5231
  }
5061
5232
  catch (error) {
5062
5233
  // El mensaje de Node lleva la ruta absoluta: al cliente solo le llega el código, y a
@@ -5114,6 +5285,23 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
5114
5285
  });
5115
5286
  respuesta.end(leido.datos);
5116
5287
  });
5288
+ /**
5289
+ * Una conversación de un proyecto, resuelta por el SERVIDOR a partir de los IDS que llegan del cable: el nombre
5290
+ * sale del listado del entorno activo, la raíz la compone el vestíbulo y la sesión tiene que estar en el ÍNDICE
5291
+ * del DISCO de ese proyecto. Nunca se lee nada que el cliente nombre. La comparten `leerSesion` y los artefactos
5292
+ * de una conversación que se LEE sin consola (una tarea finalizada): dos copias de esta barrera divergirían.
5293
+ */
5294
+ const sesionDelDisco = (proyecto, sesion) => {
5295
+ const identidad = proyectos.find((p) => p.id === proyecto);
5296
+ if (entornoElegido === undefined || identidad === undefined)
5297
+ return { motivo: "ese proyecto no está en el listado del entorno activo" };
5298
+ const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
5299
+ if (!esProyectoEnDisco(raiz))
5300
+ return { motivo: "ese proyecto no está descargado en este equipo" };
5301
+ if (typeof sesion !== "string" || !listarSesiones(raiz).some((s) => s.id === sesion))
5302
+ return { motivo: "esa conversación no está en el proyecto" };
5303
+ return { raiz, sesion };
5304
+ };
5117
5305
  /**
5118
5306
  * Lo que la pestaña Soporte necesita de UN proyecto, compuesto por el SERVIDOR a partir del
5119
5307
  * id: la raíz sale del listado del entorno activo (como `resumen` y `borrar`), las tareas se
@@ -6008,20 +6196,13 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
6008
6196
  respuesta.writeHead(409, { "content-type": "application/json" });
6009
6197
  respuesta.end(JSON.stringify({ motivo }));
6010
6198
  };
6011
- const identidad = proyectos.find((p) => p.id === mensaje.proyecto);
6012
- const sesion = mensaje.sesion;
6013
- if (entornoElegido === undefined || identidad === undefined)
6014
- return negar("ese proyecto no está en el listado del entorno activo");
6015
- const raiz = vestibulo.raizDeProyecto(entornoElegido, identidad.nombre);
6016
- if (!esProyectoEnDisco(raiz))
6017
- return negar("ese proyecto no está descargado en este equipo");
6018
6199
  // El índice del DISCO, la misma fuente que lee `reabrirSesion` justo debajo.
6019
- if (typeof sesion !== "string" || !listarSesiones(raiz).some((s) => s.id === sesion)) {
6020
- return negar("esa conversación no está en el proyecto");
6021
- }
6200
+ const resuelta = sesionDelDisco(mensaje.proyecto, mensaje.sesion);
6201
+ if ("motivo" in resuelta)
6202
+ return negar(resuelta.motivo);
6022
6203
  let actos;
6023
6204
  try {
6024
- actos = reabrirSesion(raiz, sesion).actos;
6205
+ actos = reabrirSesion(resuelta.raiz, resuelta.sesion).actos;
6025
6206
  }
6026
6207
  catch (error) {
6027
6208
  contar(error);
@@ -6210,6 +6391,82 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
6210
6391
  respuesta.end();
6211
6392
  return;
6212
6393
  }
6394
+ if (typeof mensaje === "object" &&
6395
+ mensaje !== null &&
6396
+ mensaje.clase === "gestor" &&
6397
+ (mensaje.accion === "estadosDelFlujo" || mensaje.accion === "ajustarVinculo")) {
6398
+ /**
6399
+ * El MAPEO de estados y el disparo del vínculo del proyecto ABIERTO (la puerta 2), contestados en la propia
6400
+ * respuesta. `estadosDelFlujo` lee del gestor los estados de su flujo y propone el mapeo por categoría;
6401
+ * `ajustarVinculo` lo guarda, comprobando que cada estado elegido EXISTE en ese flujo —un nombre que no está
6402
+ * dejaría el disparo mudo—, y vuelve a emitir el estado del gestor.
6403
+ */
6404
+ const negar = (motivo) => {
6405
+ respuesta.writeHead(409, { "content-type": "application/json" });
6406
+ respuesta.end(JSON.stringify({ motivo }));
6407
+ };
6408
+ const abierto = vestibulo.proyectoAbierto();
6409
+ if (abierto === undefined)
6410
+ return negar("no hay ningún proyecto abierto");
6411
+ let vinculo;
6412
+ try {
6413
+ vinculo = cargar(abierto.raiz).config.proyecto?.gestorDeTareas;
6414
+ }
6415
+ catch {
6416
+ return negar("no se pudo leer la configuración del proyecto");
6417
+ }
6418
+ if (vinculo === undefined)
6419
+ return negar("este proyecto no tiene un gestor de tareas vinculado");
6420
+ const g = gestorDe(vinculo.conector);
6421
+ if (g?.estados === undefined)
6422
+ return negar("este gestor no sabe decir los estados de su flujo");
6423
+ let lista;
6424
+ try {
6425
+ lista = await g.estados(vinculo);
6426
+ }
6427
+ catch (error) {
6428
+ return negar(`no se pudieron leer los estados: ${motivoDelGestor(error)}`);
6429
+ }
6430
+ if (mensaje.accion === "estadosDelFlujo") {
6431
+ respuesta.writeHead(200, { "content-type": "application/json" });
6432
+ respuesta.end(JSON.stringify({ lista, propuesto: mapeoPropuesto(lista), ...(vinculo.estados === undefined ? {} : { actual: vinculo.estados }) }));
6433
+ return;
6434
+ }
6435
+ const pedido = mensaje;
6436
+ if (pedido.revisarEncargo !== undefined && typeof pedido.revisarEncargo !== "boolean")
6437
+ return negar("petición mal formada");
6438
+ if (typeof pedido.automatico !== "boolean" || typeof pedido.estados !== "object" || pedido.estados === null) {
6439
+ return negar("petición mal formada");
6440
+ }
6441
+ const estados = {};
6442
+ for (const papel of ["preparar", "lanzar", "terminar"]) {
6443
+ const e = pedido.estados[papel];
6444
+ if (e === undefined || e === "")
6445
+ continue;
6446
+ if (typeof e !== "string" || !lista.some((x) => esEstado(x.nombre, e)))
6447
+ return negar(`«${String(e)}» no es un estado del flujo de este proyecto`);
6448
+ estados[papel] = lista.find((x) => esEstado(x.nombre, e)).nombre;
6449
+ }
6450
+ // Encender el disparo pide saber cuándo LANZAR: sin ese estado no haría nada y parecería roto.
6451
+ if (pedido.automatico && estados.lanzar === undefined)
6452
+ return negar("para encender el disparo, elige el estado que lanza la tarea");
6453
+ const { estados: _antes, automatico: _a, revisarEncargo: _r, ...resto } = vinculo;
6454
+ try {
6455
+ guardarGestorDeProyecto(abierto.raiz, {
6456
+ ...resto,
6457
+ ...(Object.keys(estados).length === 0 ? {} : { estados }),
6458
+ ...(pedido.automatico ? { automatico: true } : {}),
6459
+ ...(pedido.revisarEncargo === true ? { revisarEncargo: true } : {}),
6460
+ });
6461
+ }
6462
+ catch (error) {
6463
+ return negar(`no se pudo guardar en la configuración del proyecto (${codigoDe(error) ?? "su config.json no se puede leer"})`);
6464
+ }
6465
+ void atenderGestor({ clase: "gestor", accion: "estado" }).catch(contar);
6466
+ respuesta.writeHead(204);
6467
+ respuesta.end();
6468
+ return;
6469
+ }
6213
6470
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "gestor") {
6214
6471
  // Los campos se comprueban por ACCIÓN, como `modeloDelCambio`: un mensaje mal formado
6215
6472
  // se contesta con su error, no revienta dentro de `atenderGestor`.
@@ -6272,6 +6529,36 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
6272
6529
  respuesta.end();
6273
6530
  return;
6274
6531
  }
6532
+ if (typeof mensaje === "object" &&
6533
+ mensaje !== null &&
6534
+ mensaje.clase === "artefacto" &&
6535
+ typeof mensaje.nombre === "string" &&
6536
+ (mensaje.proyecto !== undefined || mensaje.sesion !== undefined)) {
6537
+ // El artefacto de una conversación que se LEE sin consola: se contesta en la PROPIA respuesta y no por el cable,
6538
+ // donde el store lo guardaría por ruta y pisaría el de la conversación abierta con el mismo nombre.
6539
+ const resuelta = sesionDelDisco(mensaje.proyecto, mensaje.sesion);
6540
+ const negar = (motivo) => {
6541
+ respuesta.writeHead(409, { "content-type": "application/json" });
6542
+ respuesta.end(JSON.stringify({ motivo }));
6543
+ };
6544
+ if ("motivo" in resuelta)
6545
+ return negar(resuelta.motivo);
6546
+ if (!esRutaDeArtefacto(`${RUTA_ARTEFACTOS}${mensaje.nombre}`))
6547
+ return negar("ese nombre no es de un artefacto");
6548
+ if (opciones.leerArtefacto === undefined)
6549
+ return negar("esta ejecución no puede leer los artefactos");
6550
+ let fichero;
6551
+ try {
6552
+ fichero = await opciones.leerArtefacto(resuelta.raiz, resuelta.sesion, mensaje.nombre);
6553
+ }
6554
+ catch (error) {
6555
+ informar(`no se pudo leer el artefacto «${mensaje.nombre}» (${codigoDe(error)})`);
6556
+ return negar("no se pudo leer el artefacto");
6557
+ }
6558
+ respuesta.writeHead(200, { "content-type": "application/json" });
6559
+ respuesta.end(JSON.stringify(fichero));
6560
+ return;
6561
+ }
6275
6562
  if (typeof mensaje === "object" && mensaje !== null && mensaje.clase === "artefacto" && typeof mensaje.nombre === "string") {
6276
6563
  void atenderArtefacto(mensaje.nombre).catch(contar);
6277
6564
  respuesta.writeHead(204);
@@ -6423,6 +6710,34 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
6423
6710
  respuesta.end();
6424
6711
  return;
6425
6712
  }
6713
+ if (mensaje.accion === "mirarGestor") {
6714
+ // «Mirar ahora»: una pasada del sondeo de la puerta 2, contestada con lo que hizo. Solo lo hace el proceso que
6715
+ // ejecuta las tareas; en otro, el sondeo no hace nada y lo dice el resultado vacío.
6716
+ if (opciones.mirarGestor === undefined) {
6717
+ respuesta.writeHead(409, { "content-type": "application/json" });
6718
+ respuesta.end(JSON.stringify({ motivo: "esta ejecución no mira el gestor de tareas" }));
6719
+ return;
6720
+ }
6721
+ const r = await opciones.mirarGestor();
6722
+ emitirTareas();
6723
+ respuesta.writeHead(200, { "content-type": "application/json" });
6724
+ respuesta.end(JSON.stringify(r));
6725
+ return;
6726
+ }
6727
+ if (mensaje.accion === "destino") {
6728
+ /**
6729
+ * Dónde vivirá una tarea NUEVA de ese proyecto: con qué gestor, si lo tiene vinculado y sabe crear. Lo pregunta la
6730
+ * ventana «Nueva tarea» al abrirse para decir ANTES del clic qué se va a escribir —crear es la autorización—, y se
6731
+ * decide con la MISMA función que `crear`, así que lo dicho y lo hecho no pueden divergir. Ni red ni gestor: lee el
6732
+ * `config.json` del proyecto. Del vínculo cruza el conector y el nombre del proyecto del gestor, nunca el sitio.
6733
+ */
6734
+ const destino = gestorParaTareaNueva(mensaje.proyecto);
6735
+ respuesta.writeHead(200, { "content-type": "application/json" });
6736
+ respuesta.end(JSON.stringify(destino === undefined
6737
+ ? {}
6738
+ : { gestor: { conector: destino.vinculo.conector, proyecto: destino.vinculo.nombreDelProyecto ?? destino.vinculo.proyecto } }));
6739
+ return;
6740
+ }
6426
6741
  if (mensaje.accion === "regenerarEncargo" && opciones.colaDeTareas !== undefined) {
6427
6742
  /**
6428
6743
  * Volver a PREPARAR el encargo de una tarea que no ha arrancado, desde su petición editada
@@ -6529,6 +6844,15 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
6529
6844
  respuesta.end(JSON.stringify({ motivo: "se está cambiando la carpeta de los proyectos: crea la tarea cuando termine" }));
6530
6845
  return;
6531
6846
  }
6847
+ // Con gestor vinculado, la tarea vive en él: se contesta en la propia respuesta (la ventana no se cierra hasta saber
6848
+ // si el gestor la creó). Sin él, el camino de siempre.
6849
+ const enGestor = mensaje.accion === "crear" ? gestorParaTareaNueva(mensaje.proyecto) : undefined;
6850
+ if (mensaje.accion === "crear" && enGestor !== undefined) {
6851
+ const r = await atenderCrearTareaEnGestor(enGestor, mensaje.peticion, mensaje.encargo, mensaje.borrador);
6852
+ respuesta.writeHead(r.estado, { "content-type": "application/json" });
6853
+ respuesta.end(JSON.stringify(r.cuerpo));
6854
+ return;
6855
+ }
6532
6856
  if (mensaje.accion === "crear" && opciones.colaDeTareas !== undefined) {
6533
6857
  const creada = atenderCrearTarea(mensaje.proyecto, mensaje.peticion, mensaje.encargo, mensaje.borrador);
6534
6858
  if (creada !== undefined) {
@@ -7098,6 +7422,40 @@ export function montarRutas(servidor, vestibulo, opciones = {}) {
7098
7422
  });
7099
7423
  return { emitirTareas, cerrarRemoto: apagarRemoto };
7100
7424
  }
7425
+ /**
7426
+ * El sondeo de la puerta 2 CABLEADO: extraído de `arrancarConsolaWeb` para poder probar la composición de verdad (el
7427
+ * patrón de fallo de siempre: una regla de producción en un cierre que todos los tests doblan). Dos cosas que solo se
7428
+ * ven aquí: lo que el sondeo guarda va por la cola ENVUELTA (así el reflejo en el gestor dispara), y tras guardar se
7429
+ * EMITE la cola —el corredor solo emite en sus propias escrituras, y una tarea que no puede arrancar todavía (la
7430
+ * persona tiene la consola de ese proyecto abierta) no salía en la pantalla—. Lo que falla en una pasada, también la
7431
+ * primera, se dice.
7432
+ */
7433
+ export function construirSondeoCableado(o) {
7434
+ const sondeo = crearSondeoDelGestor({
7435
+ copias: o.copias,
7436
+ gestorDe: o.gestorDe,
7437
+ ...(o.augmentar === undefined ? {} : { augmentar: o.augmentar }),
7438
+ cola: o.cola,
7439
+ revisar: () => {
7440
+ o.corredor.revisar();
7441
+ o.emitirTareas();
7442
+ },
7443
+ esDueño: () => o.corredor.corriendoAqui(),
7444
+ motivoDe: motivoDeGestorParaElCable,
7445
+ informar: o.informar,
7446
+ });
7447
+ const decir = (r) => {
7448
+ for (const f of r.fallos)
7449
+ o.informar(`el gestor de ${f.proyecto}${f.clave === undefined ? "" : ` (${f.clave})`}: ${f.motivo}`);
7450
+ if (r.preparadas.length > 0)
7451
+ o.emitirTareas();
7452
+ };
7453
+ return {
7454
+ ...sondeo,
7455
+ // «Mirar ahora» y la pasada del arranque dicen sus fallos igual que el reloj.
7456
+ mirar: () => sondeo.mirar().then((r) => (decir(r), r)),
7457
+ };
7458
+ }
7101
7459
  /**
7102
7460
  * Construye el corredor de tareas y la costura que lo conecta al cable.
7103
7461
  *
@@ -7743,7 +8101,27 @@ export function ajusteDeGestorCableado(ajuste) {
7743
8101
  };
7744
8102
  }
7745
8103
  export function construirCorredorDeTareasCableado(opciones) {
7746
- const disco = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
8104
+ const crudo = opciones.tareasFabrica === undefined ? undefined : opciones.tareasFabrica(opciones.informar);
8105
+ /**
8106
+ * El almacén con el `guardar` VIGILADO: un único punto por el que pasan todos los cambios de estado (corredor,
8107
+ * reconciliación, rutas, feedback), y de ahí el reflejo en el gestor. Envuelto AQUÍ, donde nace, para que el
8108
+ * corredor y las rutas (`opcionesDeMontaje.colaDeTareas`) compartan el mismo.
8109
+ */
8110
+ const reflejo = crudo === undefined || opciones.gestorDeTareas === undefined
8111
+ ? undefined
8112
+ : crearReflejoEnElGestor({
8113
+ gestorDe: opciones.gestorDeTareas,
8114
+ datosDeCierre: opciones.datosDeCierreDeTarea ?? datosDeCierreDeTarea,
8115
+ // Escribe en disco y AVISA al cable: el corredor solo emite en sus propias escrituras, y sin esto el aviso
8116
+ // («no se pudo pasar KAN-1 a EN CURSO») no salía en la fila hasta que cambiara otra cosa.
8117
+ anotarAviso: (id, aviso) => {
8118
+ anotarAvisoEnElDisco(crudo)(id, aviso);
8119
+ emitirCambioDeTareas?.();
8120
+ },
8121
+ motivoDe: motivoDeGestorParaElCable,
8122
+ informar: opciones.informar,
8123
+ });
8124
+ const disco = crudo === undefined ? undefined : reflejo === undefined ? crudo : conReflejoAlGuardar(crudo, reflejo);
7747
8125
  /**
7748
8126
  * El tope de concurrencia se LEE de `settings.json` en cada llamada, nunca se cachea —la
7749
8127
  * misma disciplina que `ajustesDeDispositivos` (más abajo) y que `sinAprobacion`: la
@@ -7986,6 +8364,8 @@ export async function arrancarConsolaWeb(opciones) {
7986
8364
  vestibulo: conVestibulo,
7987
8365
  tareasFabrica: opciones.tareas,
7988
8366
  informar,
8367
+ // El MISMO gestor memoizado que el panel: con él, una tarea con ticket se refleja en su gestor al cambiar de estado.
8368
+ gestorDeTareas: conectoresYGestor.gestorDeTareas,
7989
8369
  olvidarHiloDeSesion: olvidarMemoriaDeHilo,
7990
8370
  /**
7991
8371
  * El juez de QA de las tareas, con el papel `afilado` — el que `core/modelos.ts` le
@@ -8040,6 +8420,19 @@ export async function arrancarConsolaWeb(opciones) {
8040
8420
  escribir(`${aviso}\n`);
8041
8421
  abierto.consola.consola.escribir(`${aviso}\n`);
8042
8422
  }
8423
+ /** El aumentador: el de «Nueva tarea» (por las rutas) y el de preparar encargos del sondeo del gestor. Ver su
8424
+ * comentario en las opciones de `montarRutas`. */
8425
+ const augmentar = augmentacionCableada({
8426
+ aumentador: opciones.guion === true
8427
+ ? new AumentadorGuionizado()
8428
+ : crearAumentador({
8429
+ invocar: (papel, prompt, raiz) => invocarParaAumentar(new Modelos(fuentesDelJuez(raiz), proveedoresPersonalizados))(papel, prompt, raiz),
8430
+ }),
8431
+ contexto: contextoDelProyecto,
8432
+ });
8433
+ /** El sondeo de la puerta 2: nace con el corredor (más abajo); «Mirar ahora» llega por las rutas, montadas antes. */
8434
+ let sondeo;
8435
+ const espacioDeTrabajo = ajusteDeWorkspaceCableado({ casa: homedir() });
8043
8436
  const cable = montarRutas(servidor, vestibulo, {
8044
8437
  informar,
8045
8438
  // El interruptor se lee de `settings.json` (o `XONECODE_REMOTO=1`) en cada uso. Sin el
@@ -8239,14 +8632,8 @@ export async function arrancarConsolaWeb(opciones) {
8239
8632
  * - **Con `--guion` se monta el DOBLE**, no el real: esa bandera significa «sin gastar
8240
8633
  * ni conectar», y el doble dice en el propio encargo que es de pega (`[DOBLE]`).
8241
8634
  */
8242
- augmentar: augmentacionCableada({
8243
- aumentador: opciones.guion === true
8244
- ? new AumentadorGuionizado()
8245
- : crearAumentador({
8246
- invocar: (papel, prompt, raiz) => invocarParaAumentar(new Modelos(fuentesDelJuez(raiz), proveedoresPersonalizados))(papel, prompt, raiz),
8247
- }),
8248
- contexto: contextoDelProyecto,
8249
- }),
8635
+ augmentar,
8636
+ mirarGestor: async () => sondeo?.mirar() ?? { preparadas: [], lanzadas: [], fallos: [] },
8250
8637
  });
8251
8638
  // Con las rutas ya montadas: la reconciliación y el primer despacho cambian la cola, y
8252
8639
  // quien conecte después la recibe entera en su ráfaga de bienvenida.
@@ -8254,6 +8641,23 @@ export async function arrancarConsolaWeb(opciones) {
8254
8641
  // `cable.emitirTareas` y arranca en el orden correcto, así que no hay manera de llamarlo
8255
8642
  // mal desde aquí.
8256
8643
  await arrancarCorredorConectado(cable);
8644
+ /**
8645
+ * La puerta 2: el gestor de tareas DISPARA tareas (`agent/tareas/sondeoDelGestor.ts`). Solo con cola y corredor, y
8646
+ * solo hace algo en el proceso que EJECUTA las tareas. Una pasada al arrancar recoge lo que se movió con esto parado.
8647
+ */
8648
+ if (corredor !== undefined && opcionesDeTareas.colaDeTareas !== undefined) {
8649
+ sondeo = construirSondeoCableado({
8650
+ corredor,
8651
+ cola: opcionesDeTareas.colaDeTareas,
8652
+ copias: () => copiasConGestor(espacioDeTrabajo.workspace()),
8653
+ gestorDe: conectoresYGestor.gestorDeTareas,
8654
+ augmentar,
8655
+ emitirTareas: () => cable.emitirTareas(),
8656
+ informar,
8657
+ });
8658
+ sondeo.arrancar();
8659
+ void sondeo.mirar();
8660
+ }
8257
8661
  /**
8258
8662
  * Con qué código corre ESTE proceso, antes que la URL.
8259
8663
  *