@ingeniomaps/cauce 0.34.0 → 0.35.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,43 @@ 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.35.0] - 2026-08-19
18
+
19
+ ### Añadido
20
+
21
+ - **`ops destroy <ops-root>`: sacar una instancia entera, en un comando.** Enumera qué se pierde
22
+ —épicas, cola, trabajo terminado con su evidencia, acciones humanas pendientes, el contexto escrito— y
23
+ para. Sólo un segundo intento con `--force` desinstala cada runner cableado y después borra la
24
+ instancia. El orden no es negociable: borrar la carpeta antes que el wiring deja al runner ejecutando
25
+ guards que ya no existen, que es exactamente lo que pasaba haciéndolo a mano. También como
26
+ `make destroy`, que muestra el resumen sin borrar.
27
+
28
+ ### Corregido
29
+
30
+ - **Una épica que nadie va a leer deja de pasar por válida.** Una corrida real escribió
31
+ `roadmap/epic-001.md`, sin slug: el parser busca `epic-NNN-<slug>.md`, así que `check` respondía
32
+ «planning válido: 0 épica(s)» con la épica ahí, y `autobuild` nunca habría encontrado trabajo en ella.
33
+ Renombrarla destapó dos errores que el silencio tapaba —un `status` inválido y un criterio sin historia
34
+ que lo cubriera—. Ahora cualquier archivo del roadmap que se llame como una épica y el parser no vaya a
35
+ leer es un error que dice cómo nombrarlo.
36
+
37
+ - **`check` avisa cuando una dimensión del molde desapareció de `organization/`.** Un agente que reescribe
38
+ esos archivos se queda con el contenido y pierde la estructura: el resultado se lee entero y completo, y
39
+ la dimensión que falta no la va a reclamar nadie. Va como advertencia —esos archivos son de la empresa y
40
+ reestructurarlos a propósito es legítimo—, y agregar secciones propias no dice nada.
41
+
42
+ - **Volver a una versión anterior deja de anunciarse como una actualización.** `upgrade --check` decía
43
+ «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
44
+ las entradas que dejás de tener en vez de las que ganarías.
45
+
46
+ ### Cambiado
47
+
48
+ - **Los runners sin recorrido ejecutable llevan una lista de salida.** Codex, Gemini y Antigravity operan
49
+ el arranque leyendo instrucciones, y una corrida real falló cinco puntos de una vez: preguntas en
50
+ formulario, una épica sin slug, `organization/` reestructurado, `HUMAN_ACTIONS.md` vacío y `ops` listado
51
+ como servicio del producto. Todos del mismo lado: lo que no deja un archivo visible. La lista dice qué
52
+ se comprueba mirando el disco, que es lo que la prosa pidiendo cuidado no consigue.
53
+
17
54
  ## [0.34.0] - 2026-08-19
18
55
 
19
56
  ### 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,27 @@ 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
+ Recién ahí, `node {{OPS_DIR}}tools/ops.js check planning`. Si sale con advertencias, leelas: son
64
+ exactamente estas cosas.
@@ -53,6 +53,12 @@ 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.
61
+
56
62
  ## Los equipos
57
63
 
58
64
  Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
@@ -46,6 +46,12 @@ 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.
54
+
49
55
  Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
50
56
  trabajo desde INBOX.
51
57
 
@@ -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.35.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 .