@ingeniomaps/cauce 0.34.0 → 0.36.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.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,61 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
14
14
  unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
15
15
  diseño — eso vive en el commit y en el código.
16
16
 
17
+ ## [0.36.0] - 2026-08-19
18
+
19
+ ### Corregido
20
+
21
+ - **`automation doctor` ejecuta el puente del runner en vez de darlo por bueno.** Instalado no es
22
+ operativo: un puente que el runner no puede lanzar falla cerrado y niega cada llamada a herramienta,
23
+ y `doctor` decía «adaptador operativo» porque los archivos estaban en su lugar. Ahora lo invoca como
24
+ lo hace el runner —una vez desde la raíz del workspace y otra desde otra carpeta, porque una ruta
25
+ relativa en su configuración deja de resolver cuando el cwd no es el que suponía— y un puente que no
26
+ responde es error, no advertencia.
27
+
28
+ ### Cambiado
29
+
30
+ - **La lista de salida termina en el chequeo más barato: `ops onboard`.** Si vuelve a ofrecer la
31
+ pregunta de apertura, la instancia sigue vacía y no hay nada que informar. Una corrida real entregó
32
+ cuatro puntos en pasado —«registrado», «documentado», «creada la primera épica»— sobre archivos que en
33
+ el disco seguían siendo el molde. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
34
+
35
+ ## [0.35.0] - 2026-08-19
36
+
37
+ ### Añadido
38
+
39
+ - **`ops destroy <ops-root>`: sacar una instancia entera, en un comando.** Enumera qué se pierde
40
+ —épicas, cola, trabajo terminado con su evidencia, acciones humanas pendientes, el contexto escrito— y
41
+ para. Sólo un segundo intento con `--force` desinstala cada runner cableado y después borra la
42
+ instancia. El orden no es negociable: borrar la carpeta antes que el wiring deja al runner ejecutando
43
+ guards que ya no existen, que es exactamente lo que pasaba haciéndolo a mano. También como
44
+ `make destroy`, que muestra el resumen sin borrar.
45
+
46
+ ### Corregido
47
+
48
+ - **Una épica que nadie va a leer deja de pasar por válida.** Una corrida real escribió
49
+ `roadmap/epic-001.md`, sin slug: el parser busca `epic-NNN-<slug>.md`, así que `check` respondía
50
+ «planning válido: 0 épica(s)» con la épica ahí, y `autobuild` nunca habría encontrado trabajo en ella.
51
+ Renombrarla destapó dos errores que el silencio tapaba —un `status` inválido y un criterio sin historia
52
+ que lo cubriera—. Ahora cualquier archivo del roadmap que se llame como una épica y el parser no vaya a
53
+ leer es un error que dice cómo nombrarlo.
54
+
55
+ - **`check` avisa cuando una dimensión del molde desapareció de `organization/`.** Un agente que reescribe
56
+ esos archivos se queda con el contenido y pierde la estructura: el resultado se lee entero y completo, y
57
+ la dimensión que falta no la va a reclamar nadie. Va como advertencia —esos archivos son de la empresa y
58
+ reestructurarlos a propósito es legítimo—, y agregar secciones propias no dice nada.
59
+
60
+ - **Volver a una versión anterior deja de anunciarse como una actualización.** `upgrade --check` decía
61
+ «hay una versión más nueva: 0.33.0» contra una instancia en 0.34.0. Ahora dice que volvés, y te imprime
62
+ las entradas que dejás de tener en vez de las que ganarías.
63
+
64
+ ### Cambiado
65
+
66
+ - **Los runners sin recorrido ejecutable llevan una lista de salida.** Codex, Gemini y Antigravity operan
67
+ el arranque leyendo instrucciones, y una corrida real falló cinco puntos de una vez: preguntas en
68
+ formulario, una épica sin slug, `organization/` reestructurado, `HUMAN_ACTIONS.md` vacío y `ops` listado
69
+ como servicio del producto. Todos del mismo lado: lo que no deja un archivo visible. La lista dice qué
70
+ se comprueba mirando el disco, que es lo que la prosa pidiendo cuidado no consigue.
71
+
17
72
  ## [0.34.0] - 2026-08-19
18
73
 
19
74
  ### Añadido
package/README.md CHANGED
@@ -177,6 +177,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
177
177
  | `ops tree <planning>` | Muestra roadmap, backlog, WIP, inbox y done sin mutar nada. |
178
178
  | `ops context <planning>` | Emite el contexto mínimo de la tarea vigente para un runner. |
179
179
  | `ops upgrade <ops-root>` | Actualiza `system/` y el runtime sin tocar lo del proyecto. |
180
+ | `ops destroy <ops-root>` | Enumera qué se pierde y, con `--force`, saca wiring e instancia. |
180
181
  | `ops archive <planning> <NNN>` | Archiva el DONE de una épica cerrada de forma idempotente. |
181
182
  | `ops agents list [ops-root]` | Lista los cargos visibles resolviendo la precedencia. |
182
183
  | `ops agents fork <cargo>` | Copia un cargo del catálogo a la empresa, que pasa a mantenerlo. |
@@ -301,7 +302,10 @@ La instalación fusiona la configuración propia del runner y conserva las entra
301
302
  reemplaza los guards que el propio toolkit había registrado sueltos por el grupo que ahora los cubre, y
302
303
  lista cuáles quitó. Nada que no haya escrito el toolkit se toca.
303
304
 
304
- Para sacarlo, `automation uninstall` quita exactamente lo que Cauce entregó y sigue igual que como lo
305
+ Para sacar una instancia entera —wiring incluido— está `ops destroy`, que primero enumera qué se pierde
306
+ y sólo borra si se lo repite con `--force`: el orden importa, porque borrar la carpeta antes que el
307
+ wiring deja al runner ejecutando guards que ya no existen. Para sacar sólo el wiring y conservar la
308
+ instancia, `automation uninstall` quita exactamente lo que Cauce entregó y sigue igual que como lo
305
309
  entregó: los guards de la configuración del runner, los workflows, los punteros a cargos. Lo que no
306
310
  escribió —tus hooks, tus workflows, tus skills— queda donde está, y un archivo suyo que hayas editado se
307
311
  conserva y se nombra en la salida. Borrar la carpeta ops sin esto deja al runner ejecutando guards que ya
@@ -38,7 +38,32 @@ El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, d
38
38
  para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
39
39
  algo concreto.
40
40
 
41
- Cerrá escribiendo `epic-001` en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
41
+ Cerrá escribiendo la épica en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
42
42
  atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
43
- comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—. Validá
44
- con `node {{OPS_DIR}}tools/ops.js check planning`. **Nunca promuevas al BACKLOG**: esa firma es humana.
43
+ comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—.
44
+ **Nunca promuevas al BACKLOG**: esa firma es humana.
45
+
46
+ ## Antes de decir que terminaste
47
+
48
+ Esta lista no es un resumen de lo de arriba: es lo que se comprueba mirando el disco. Una corrida real
49
+ falló los cinco puntos sin darse cuenta, y todos del mismo lado —lo que no produce un archivo visible—.
50
+
51
+ 1. **Una pregunta por vez.** Si mandaste dos o más juntas, numeradas, hiciste un formulario. La segunda
52
+ pregunta depende de la primera respuesta: por eso se hacen de a una.
53
+ 2. **`epic-NNN-<slug>.md`**, con slug en el nombre y `status: open`. `epic-001.md` no lo lee nadie: ni
54
+ `check`, ni `tree`, ni el runner que busca trabajo. Existe y no existe a la vez.
55
+ 3. **Las secciones de `{{OPS_DIR}}organization/` son las del molde.** Escribí adentro de ellas; agregá las tuyas
56
+ abajo si hacen falta. Reemplazar la estructura pierde dimensiones que nadie va a reclamar después,
57
+ porque el archivo se lee completo.
58
+ 4. **`HUMAN_ACTIONS.md` tiene filas.** Una por credencial nombrada en el inventario, una por sistema
59
+ externo o MCP, una por la autoridad de push. Si quedó vacío, no es que no hubiera nada: es que lo que
60
+ no te corresponde se perdió en vez de quedar escrito para alguien.
61
+ 5. **`ops` no es un servicio del producto.** No va en el mapa real: es la instancia desde la que trabajás.
62
+
63
+ 6. **Corré `node {{OPS_DIR}}tools/ops.js onboard`.** Si te vuelve a ofrecer la pregunta de apertura, la
64
+ instancia sigue vacía: no escribiste nada, y no hay nada que informar. Una corrida real entregó un
65
+ resumen en pasado —«registrado», «documentado», «creada la primera épica»— sobre archivos que en el
66
+ disco seguían siendo el molde. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
67
+
68
+ Recién ahí, `node {{OPS_DIR}}tools/ops.js check planning`. Si sale con advertencias, leelas: son
69
+ exactamente estas cosas.
@@ -53,6 +53,14 @@ El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, d
53
53
  para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
54
54
  algo concreto.
55
55
 
56
+ Antes de darlo por terminado, comprobá cinco cosas mirando el disco: preguntaste de a una y no en
57
+ formulario; la épica se llama `epic-NNN-<slug>.md` con `status: open` —`epic-001.md` no lo lee nadie—;
58
+ las secciones de `{{OPS_DIR}}organization/` siguen siendo las del molde y lo tuyo se agregó adentro;
59
+ `{{OPS_DIR}}planning/HUMAN_ACTIONS.md` tiene una fila por credencial, por externo y por la autoridad de
60
+ push; y `ops` no figura como servicio del producto en el mapa. Cerrá corriendo
61
+ `node {{OPS_DIR}}tools/ops.js onboard`: si te vuelve a ofrecer la pregunta de apertura, no escribiste
62
+ nada y no hay nada que informar. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
63
+
56
64
  ## Los equipos
57
65
 
58
66
  Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
@@ -46,6 +46,14 @@ El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, d
46
46
  para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
47
47
  algo concreto.
48
48
 
49
+ Antes de darlo por terminado, comprobá cinco cosas mirando el disco: preguntaste de a una y no en
50
+ formulario; la épica se llama `epic-NNN-<slug>.md` con `status: open` —`epic-001.md` no lo lee nadie—;
51
+ las secciones de `{{OPS_DIR}}organization/` siguen siendo las del molde y lo tuyo se agregó adentro;
52
+ `{{OPS_DIR}}planning/HUMAN_ACTIONS.md` tiene una fila por credencial, por externo y por la autoridad de
53
+ push; y `ops` no figura como servicio del producto en el mapa. Cerrá corriendo
54
+ `node {{OPS_DIR}}tools/ops.js onboard`: si te vuelve a ofrecer la pregunta de apertura, no escribiste
55
+ nada y no hay nada que informar. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
56
+
49
57
  Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
50
58
  trabajo desde INBOX.
51
59
 
@@ -377,6 +377,46 @@ function legacyGuardWiring(config) {
377
377
  return superseded
378
378
  }
379
379
 
380
+ // Ejecuta el puente del runner tal como él lo invoca, y desde otra carpeta. Instalado no es lo mismo que
381
+ // operativo: un bridge que el runner no puede lanzar —porque su ruta es relativa y el cwd es otro, o
382
+ // porque falta node— falla cerrado y niega cada llamada a herramienta. `doctor` veía los archivos en su
383
+ // lugar y decía «operativo» mientras nada respondía.
384
+ function probeBridge(paths, runner) {
385
+ const bridge = (runner.artifacts || []).find((item) => item.target.endsWith('hook.js'))
386
+ if (!bridge) return []
387
+ const script = path.resolve(paths.install, bridge.target)
388
+ if (!fs.existsSync(script)) return []
389
+ let config = {}
390
+ try { config = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8')) } catch { return [] }
391
+ const eventos = [...new Set(
392
+ JSON.stringify(config).match(/hook\.js ([a-z-]+)/g) || [],
393
+ )].map((entrada) => entrada.split(' ')[1])
394
+ if (!eventos.length) return []
395
+
396
+ const problemas = []
397
+ // Desde la raíz y desde una carpeta de adentro: si el runner lanza el hook con otro cwd, la ruta
398
+ // relativa de su configuración deja de resolver y eso hay que verlo acá, no en la primera sesión.
399
+ const desde = [paths.install, path.dirname(script)]
400
+ for (const evento of eventos) {
401
+ for (const cwd of desde) {
402
+ const payload = JSON.stringify({ toolCall: { args: { CommandLine: 'ls', Cwd: paths.install } } })
403
+ const result = spawnSync(process.execPath, [script, evento], { cwd, input: payload, encoding: 'utf8' })
404
+ let respuesta = {}
405
+ try { respuesta = JSON.parse((result.stdout || '').trim()) } catch { respuesta = {} }
406
+ // Cada evento tiene su respuesta sana: `stop` cierra la sesión —eso es funcionar— y el resto deja
407
+ // pasar. El puente falla cerrado, así que `deny` en una llamada inocua es que algo se rompió antes
408
+ // de poder juzgarla, y `continue` es el camino de error del propio `stop`.
409
+ const sana = evento === 'stop' ? 'stop' : 'allow'
410
+ if (respuesta.decision === sana) continue
411
+ const salida = (result.stderr || 'sin respuesta').trim().split('\n')[0]
412
+ const motivo = respuesta.reason
413
+ || (respuesta.decision ? `respondió ${respuesta.decision}` : salida)
414
+ problemas.push(`${bridge.target} ${evento} desde ${path.basename(cwd)}/: ${motivo}`)
415
+ }
416
+ }
417
+ return problemas
418
+ }
419
+
380
420
  function doctor(root, name, output = console) {
381
421
  const runner = runnerManifest(root, name)
382
422
  const paths = runnerPaths(root, name, runner)
@@ -441,6 +481,9 @@ function doctor(root, name, output = console) {
441
481
  warnings.push(`${stale.length} cargo(s) desactualizados en ${runner.roleSkills}: reinstalá el adaptador`)
442
482
  }
443
483
  }
484
+ // Un puente que no responde niega cada llamada del runner: es error, no advertencia.
485
+ for (const problema of probeBridge(paths, runner)) errors.push(problema)
486
+
444
487
  const executable = spawnSync(
445
488
  'sh',
446
489
  ['-c', 'command -v "$1" >/dev/null 2>&1', 'runner-doctor', runner.command],
@@ -20,6 +20,7 @@ const FLAGS = {
20
20
  tree: ['--json', '--no-color'],
21
21
  context: ['--json'],
22
22
  upgrade: ['--check', '--force'],
23
+ destroy: ['--force'],
23
24
  archive: [],
24
25
  agents: ['--json', '--own', '--system'],
25
26
  integration: ['--fixture'],
package/engine/cli/ops.js CHANGED
@@ -47,6 +47,7 @@ function usage() {
47
47
  ops tree <planning-dir> [--no-color] [--json]
48
48
  ops context <planning-dir> [--json]
49
49
  ops upgrade <ops-root> [--check] [--force]
50
+ ops destroy <ops-root> [--force]
50
51
  ops archive <planning-dir> <NNN>
51
52
  ops integration list <ops-root>
52
53
  ops integration enable <ops-root> <provider>
@@ -440,6 +441,30 @@ function onboard(rootArg, cli, runner = '') {
440
441
  console.log(' Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.')
441
442
  }
442
443
 
444
+ // Qué dimensiones enumera el molde de `organization/` y cuáles dejaron de estar. Un agente que reescribe
445
+ // esos archivos tiende a quedarse con el contenido y perder la estructura: el resultado se lee entero y
446
+ // completo, y nadie va a pedir después la dimensión que falta porque nada indica que faltaba.
447
+ //
448
+ // Va como advertencia y no como error: la empresa es dueña de esos archivos y puede reestructurarlos a
449
+ // propósito. Lo que no puede pasar es que una dimensión desaparezca sin que se vea.
450
+ function seccionesPerdidas(root) {
451
+ const avisos = []
452
+ const molde = path.join(PROJECT_ROOT, 'template', 'organization')
453
+ for (const name of ['company.md', 'product.md']) {
454
+ const propio = path.join(root, 'organization', name)
455
+ if (!fs.existsSync(propio) || !fs.existsSync(path.join(molde, name))) continue
456
+ const titulos = (text) => new Set((text.match(/^##\s+(.+)$/gm) || []).map((line) => line.trim()))
457
+ const esperadas = titulos(fs.readFileSync(path.join(molde, name), 'utf8'))
458
+ const presentes = titulos(fs.readFileSync(propio, 'utf8'))
459
+ const faltan = [...esperadas].filter((titulo) => !presentes.has(titulo))
460
+ if (!faltan.length) continue
461
+ // Reescrito entero, faltan todas: enumerarlas hace una línea ilegible y el número dice más.
462
+ const lista = faltan.length > 3 ? `${faltan.slice(0, 3).join(', ')} y ${faltan.length - 3} más` : faltan.join(', ')
463
+ avisos.push(`organization/${name}: sin ${lista} — el molde las trae y acá no están`)
464
+ }
465
+ return avisos
466
+ }
467
+
443
468
  function check(dir, cli) {
444
469
  const root = path.resolve(dir || '.')
445
470
  const errors = []
@@ -552,6 +577,8 @@ function check(dir, cli) {
552
577
  const FK = require('../agents/fork')
553
578
  for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
554
579
 
580
+ warnings.push(...seccionesPerdidas(path.resolve(root, '..')))
581
+
555
582
  if (cli.has('--json')) {
556
583
  console.log(JSON.stringify({
557
584
  ok: !errors.length,
@@ -732,6 +759,76 @@ function instanceVersion(root) {
732
759
  } catch { return '' }
733
760
  }
734
761
 
762
+ // Qué se pierde al borrar una instancia. Se cuenta antes de tocar nada porque es lo único que vuelve
763
+ // reversible la decisión: quien lee esto todavía puede no seguir.
764
+ // Lo cuenta el mismo parser que usan `check` y `tree`, no una expresión regular propia: los moldes traen
765
+ // ejemplos comentados, y contarlos a mano anunciaba una tarea en cola y otra terminada en una instancia
766
+ // recién creada. Un aviso que exagera lo que se pierde se deja de leer igual que uno que lo minimiza.
767
+ function loQueSePierde(root) {
768
+ const planning = path.join(root, 'planning')
769
+ const cola = P.readBacklog(planning).reduce((total, hito) => total + (hito.tasks || []).length, 0)
770
+ const acciones = (() => {
771
+ try {
772
+ const texto = fs.readFileSync(path.join(planning, 'HUMAN_ACTIONS.md'), 'utf8')
773
+ return (texto.match(/^\| (?!Tarea|---)/gm) || []).length
774
+ } catch { return 0 }
775
+ })()
776
+ return {
777
+ epicas: P.readEpics(planning).length,
778
+ hechas: (P.readDone(planning).entries || []).length,
779
+ enCola: cola,
780
+ acciones,
781
+ contexto: !OB.guide(root).fresh,
782
+ runners: [...new Set(Object.keys(M.readRunners(root)).map((key) => key.split('/')[0]))],
783
+ }
784
+ }
785
+
786
+ // Saca la instancia entera y el wiring que dejó en cada runner. Existe porque la alternativa era una
787
+ // lista de pasos a mano —desinstalar cada runner y después `rm -rf`—, y una lista se ejecuta a medias:
788
+ // el orden importa, y borrar primero la carpeta deja al runner ejecutando guards que ya no existen.
789
+ //
790
+ // Nunca borra sin que alguien lo haya pedido dos veces. Lo que hay adentro —planning, organization, la
791
+ // evidencia de lo hecho— es del proyecto y no lo repone ningún `init`.
792
+ function destroy(dir, cli) {
793
+ const root = path.resolve(dir || '.')
794
+ if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
795
+ fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
796
+ }
797
+ if (O.mode(root) === 'toolkit') fail(`${root} es el toolkit: acá se fabrica Cauce, no se lo borra.`, 2)
798
+
799
+ const perdida = loQueSePierde(root)
800
+ const lineas = [
801
+ perdida.epicas && `${perdida.epicas} épica(s) en el roadmap`,
802
+ perdida.enCola && `${perdida.enCola} tarea(s) en la cola`,
803
+ perdida.hechas && `${perdida.hechas} tarea(s) terminada(s) con su evidencia`,
804
+ perdida.acciones && `${perdida.acciones} acción(es) humana(s) pendiente(s)`,
805
+ perdida.contexto && 'el contexto escrito en organization/',
806
+ ].filter(Boolean)
807
+
808
+ if (!cli.has('--force')) {
809
+ console.log(`Borrar ${root} se lleva:`)
810
+ for (const linea of lineas) console.log(` − ${linea}`)
811
+ if (!lineas.length) console.log(' − nada escrito todavía: la instancia está como salió de init')
812
+ if (perdida.runners.length) {
813
+ console.log(` y saca el wiring de: ${perdida.runners.join(', ')} (lo tuyo queda donde está)`)
814
+ }
815
+ console.log('\nNada de esto lo repone un init. Si es lo que querés: repetí con --force.')
816
+ process.exit(1)
817
+ }
818
+
819
+ for (const runner of perdida.runners) {
820
+ try { A.uninstall(root, runner, console) } catch (error) { console.error(` ${runner}: ${error.message}`) }
821
+ }
822
+ // El orden no es negociable: primero el wiring, después la carpeta. Al revés, cada llamada de
823
+ // herramienta del runner queda ejecutando un guard que ya no está.
824
+ const adentro = process.cwd() === root || process.cwd().startsWith(`${root}${path.sep}`)
825
+ fs.rmSync(root, { recursive: true, force: true })
826
+ console.log(`✓ ${root} borrado`)
827
+ // Correrlo desde adentro es lo natural —ahí está `tools/ops.js`— y deja la terminal en un directorio
828
+ // que ya no existe: el próximo comando falla con un `getcwd` que no dice nada de esto.
829
+ if (adentro) console.log(' tu terminal quedó en esa carpeta: hacé "cd .." antes del próximo comando.')
830
+ }
831
+
735
832
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
736
833
  // propias, agentes editados— queda intacto por construcción, no por comparación.
737
834
  function upgrade(dir, cli) {
@@ -760,8 +857,16 @@ function upgrade(dir, cli) {
760
857
  console.log(`= ${to}: la instancia está al día con el motor instalado`)
761
858
  return console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
762
859
  }
763
- console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
764
- printChangelog(from, to)
860
+ // Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
861
+ // una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
862
+ // contrario: no lo que se gana, sino lo que se deja.
863
+ if (CL.compare(to, from) < 0) {
864
+ console.log(`↩ volvés a ${to} desde ${from}. Esto es lo que dejás de tener:`)
865
+ printChangelog(to, from)
866
+ } else {
867
+ console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
868
+ printChangelog(from, to)
869
+ }
765
870
  for (const file of changed) console.log(` editado localmente: ${file}`)
766
871
  process.exit(1)
767
872
  }
@@ -1263,6 +1368,7 @@ async function run(cli) {
1263
1368
  else if (command === 'tree') tree(arg[1], cli)
1264
1369
  else if (command === 'context') context(arg[1], cli)
1265
1370
  else if (command === 'upgrade') upgrade(arg[1], cli)
1371
+ else if (command === 'destroy') destroy(arg[1], cli)
1266
1372
  else if (command === 'agents') agents(arg[1], arg[2], arg[3], cli)
1267
1373
  else if (command === 'archive') archive(arg[1], arg[2])
1268
1374
  else if (command === 'integration') {
@@ -64,6 +64,16 @@ function validateRoadmapStructure(dir) {
64
64
  try { entries = fs.readdirSync(roadmap, { withFileTypes: true }) } catch { return ['falta roadmap/'] }
65
65
  const errors = []
66
66
  for (const entry of entries) {
67
+ // Un archivo que se llama como una épica y no cumple el patrón no lo lee nadie: ni `check`, ni
68
+ // `tree`, ni el runner que busca trabajo. Ignorarlo en silencio es peor que rechazarlo, porque el
69
+ // planning se reporta válido mientras la épica que alguien escribió no existe para el sistema.
70
+ if (/^epic-/.test(entry.name) && !/^epic-\d{3}-/.test(entry.name)) {
71
+ errors.push(
72
+ `roadmap/${entry.name}: nadie lo lee. Una épica se nombra epic-NNN-<slug>.md, `
73
+ + 'o un directorio epic-NNN-<slug>/ con spec.md adentro.',
74
+ )
75
+ continue
76
+ }
67
77
  if (!entry.isDirectory() || !/^epic-\d{3}-/.test(entry.name)) continue
68
78
  const epicDir = path.join(roadmap, entry.name)
69
79
  if (!fs.existsSync(path.join(epicDir, 'spec.md'))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
package/template/Makefile CHANGED
@@ -1,6 +1,6 @@
1
1
  .DEFAULT_GOAL := help
2
2
 
3
- .PHONY: help check tree context upgrade automation-check
3
+ .PHONY: help check tree context upgrade destroy automation-check
4
4
  .PHONY: integration-check integration-sync require-key integration-promote
5
5
  .PHONY: require-agent agent-learn agent-propose agent-evaluate require-team team-check team-show
6
6
  .PHONY: install-claude install-codex install-gemini install-antigravity
@@ -25,6 +25,9 @@ context: ## Muestra el contexto mínimo de la tarea vigente
25
25
 
26
26
  # `init` fija la versión exacta del motor, así que `npm update` no la mueve: hay que pedir @latest.
27
27
  # Sin este primer paso `upgrade` compara contra el motor instalado y contesta «al día» para siempre.
28
+ destroy: ## Muestra qué se pierde al borrar esta instancia (borrar exige --force a mano)
29
+ @node tools/ops.js destroy .
30
+
28
31
  upgrade: ## Trae la última versión de Cauce y la aplica, sin tocar lo del proyecto
29
32
  @npm install --save-dev @ingeniomaps/cauce@latest
30
33
  @node tools/ops.js upgrade .