@ingeniomaps/cauce 0.39.0 → 0.41.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,31 @@ 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.41.0] - 2026-08-19
18
+
19
+ ### Corregido
20
+
21
+ - **`destroy` ya no borra el repositorio en modo embebido.** Ahí la instancia **es** el repositorio, así
22
+ que borrar la carpeta se llevaba el código del producto: pasó de verdad sobre un caso de prueba, y el
23
+ aviso previo enumeraba épicas y acciones humanas sin nombrar lo único irrecuperable. En embebido saca
24
+ lo que Cauce escribió —`planning/`, `organization/`, `teams/`, `integrations/`, `automatization/`,
25
+ `tools/`, la configuración y el manifiesto— y deja el repositorio. En sidecar sigue borrando la
26
+ carpeta, que ahí no contiene nada más.
27
+
28
+ ## [0.40.0] - 2026-08-19
29
+
30
+ ### Corregido
31
+
32
+ - **En modo embebido, Codex se quedaba sin instrucciones.** `AGENTS.md` es el único nombre que el runner
33
+ y la instancia comparten, y ahí `install` conservaba el archivo entero —lo correcto para algo del
34
+ proyecto—, así que el recorrido que ese runner tenía que seguir nunca llegaba a estar escrito. Ahora su
35
+ contenido se fusiona adentro, entre marcas: reinstalar reemplaza el bloque en vez de duplicarlo,
36
+ desinstalar lo saca sin llevarse el archivo, y todo lo que la empresa escribió alrededor queda intacto.
37
+
38
+ `doctor` lo tenía al revés en ese archivo: comparando el hash completo avisaba cuando el bloque estaba
39
+ bien —el texto de la empresa alrededor difiere siempre— y decía «operativo» cuando alguien lo había
40
+ borrado, que es la falla que importa.
41
+
17
42
  ## [0.39.0] - 2026-08-19
18
43
 
19
44
  ### Corregido
@@ -438,6 +438,19 @@ function doctor(root, name, output = console) {
438
438
  }
439
439
  for (const item of runner.instructions || []) {
440
440
  const resolved = { item, ...resolveItem(paths, root, name, item) }
441
+ // El archivo compartido no se compara entero: alrededor del bloque vive el texto de la empresa, así
442
+ // que su hash difiere siempre. Comparado como archivo, `doctor` avisaba cuando el bloque estaba bien
443
+ // y callaba cuando alguien lo había borrado, que es exactamente al revés.
444
+ if (esArchivoCompartido(root, resolved.target)) {
445
+ const contenido = render(resolved.source, opsPrefix(root))
446
+ if (!fs.existsSync(resolved.target)) errors.push(`falta ${item.target}`)
447
+ else if (!fs.readFileSync(resolved.target, 'utf8').includes(bloqueInicio(name))) {
448
+ errors.push(`${item.target}: no tiene las instrucciones de Cauce; reinstalá el adaptador`)
449
+ } else if (!bloqueAlDia(resolved.target, name, contenido)) {
450
+ warnings.push(`${item.target}: su bloque de Cauce quedó viejo; reinstalá el adaptador`)
451
+ }
452
+ continue
453
+ }
441
454
  // `AGENTS.md` es un nombre compartido entre herramientas y la instancia ya tiene el suyo: en
442
455
  // modo embedded el archivo del runner y el de la empresa son el mismo, y pedirle que se
443
456
  // referencie a sí mismo no significa nada.
@@ -585,6 +598,16 @@ function uninstall(root, name, output = console) {
585
598
  const resolved = { item, ...resolveItem(paths, root, name, item) }
586
599
  const key = deliveryKey(name, item.target)
587
600
  if (!fs.existsSync(resolved.target)) { delete entregado[key]; continue }
601
+ if (esArchivoCompartido(root, resolved.target)) {
602
+ const texto = fs.readFileSync(resolved.target, 'utf8')
603
+ const limpio = sinBloque(texto, name)
604
+ if (limpio !== texto.trimEnd()) {
605
+ F.atomicWrite(resolved.target, `${limpio}\n`)
606
+ quitados += 1
607
+ }
608
+ delete entregado[key]
609
+ continue
610
+ }
588
611
  const situacion = deliveryState(recorded, name, resolved, prefix)
589
612
  if (situacion === 'ajeno') { conservados.push(item.target); continue }
590
613
  removeFile(resolved.target, paths.install)
@@ -623,6 +646,41 @@ function uninstall(root, name, output = console) {
623
646
  return { removed: quitados, kept: conservados }
624
647
  }
625
648
 
649
+ // `AGENTS.md` es el único nombre que el runner y la instancia comparten: en modo embebido el archivo de
650
+ // instrucciones de Codex es el mismo que el de la empresa. Conservarlo entero —lo correcto para un
651
+ // archivo del proyecto— dejaba a ese runner sin una sola línea de Cauce, así que su contenido se
652
+ // fusiona adentro, entre marcas, y todo lo demás del archivo queda intacto.
653
+ const bloqueInicio = (name) => `<!-- cauce:${name} inicio — lo reescribe "automation install", no editar -->`
654
+ const bloqueFin = (name) => `<!-- cauce:${name} fin -->`
655
+
656
+ function esArchivoCompartido(root, target) {
657
+ return path.resolve(target) === path.resolve(root, 'AGENTS.md')
658
+ }
659
+
660
+ function sinBloque(text, name) {
661
+ const desde = text.indexOf(bloqueInicio(name))
662
+ if (desde === -1) return text
663
+ const hasta = text.indexOf(bloqueFin(name), desde)
664
+ if (hasta === -1) return text
665
+ return `${text.slice(0, desde)}${text.slice(hasta + bloqueFin(name).length)}`.trimEnd()
666
+ }
667
+
668
+ function fusionarInstruccion(file, name, contenido) {
669
+ const actual = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
670
+ const cuerpo = sinBloque(actual, name).trimEnd()
671
+ const bloque = `${bloqueInicio(name)}\n\n${contenido.trim()}\n\n${bloqueFin(name)}\n`
672
+ F.atomicWrite(file, cuerpo ? `${cuerpo}\n\n${bloque}` : bloque)
673
+ }
674
+
675
+ function bloqueAlDia(file, name, contenido) {
676
+ if (!fs.existsSync(file)) return false
677
+ const texto = fs.readFileSync(file, 'utf8')
678
+ const desde = texto.indexOf(bloqueInicio(name))
679
+ const hasta = texto.indexOf(bloqueFin(name))
680
+ if (desde === -1 || hasta === -1) return false
681
+ return texto.slice(desde + bloqueInicio(name).length, hasta).trim() === contenido.trim()
682
+ }
683
+
626
684
  function install(root, name, output = console, options = {}) {
627
685
  // `install` arma la superficie de consumo de una empresa: punteros a cada cargo, una copia de los
628
686
  // workflows y los guards. Acá los cargos y los workflows son el producto —la copia divergiría— y
@@ -677,6 +735,17 @@ function install(root, name, output = console, options = {}) {
677
735
  for (const resolved of resolvedItems) {
678
736
  const situacion = state.get(resolved)
679
737
  const propio = runner.instructions.includes(resolved.item)
738
+ if (propio && esArchivoCompartido(root, resolved.target)) {
739
+ const contenido = render(resolved.source, opsPrefix(root))
740
+ if (bloqueAlDia(resolved.target, name, contenido)) {
741
+ output.log(`= ${name}: ${resolved.item.target} ya trae sus instrucciones`)
742
+ } else {
743
+ fusionarInstruccion(resolved.target, name, contenido)
744
+ output.log(`✓ ${name}: sus instrucciones quedaron dentro de ${resolved.item.target}`)
745
+ }
746
+ entregado[deliveryKey(name, resolved.item.target)] = M.digest(resolved.target)
747
+ continue
748
+ }
680
749
  if (situacion === 'ajeno' && propio) {
681
750
  output.log(`= ${name}: conservado ${resolved.item.target} (tiene cambios tuyos)`)
682
751
  } else if (situacion === 'al día') {
package/engine/cli/ops.js CHANGED
@@ -831,13 +831,19 @@ function destroy(dir, cli) {
831
831
  perdida.contexto && 'el contexto escrito en organization/',
832
832
  ].filter(Boolean)
833
833
 
834
+ const embebido = O.mode(root) === 'embedded'
834
835
  if (!cli.has('--force')) {
835
- console.log(`Borrar ${root} se lleva:`)
836
+ console.log(embebido
837
+ ? `Sacar Cauce de ${root} se lleva:`
838
+ : `Borrar ${root} se lleva:`)
836
839
  for (const linea of lineas) console.log(` − ${linea}`)
837
840
  if (!lineas.length) console.log(' − nada escrito todavía: la instancia está como salió de init')
838
841
  if (perdida.runners.length) {
839
842
  console.log(` y saca el wiring de: ${perdida.runners.join(', ')} (lo tuyo queda donde está)`)
840
843
  }
844
+ if (embebido) {
845
+ console.log(' el código del repositorio no se toca: en modo embebido la instancia es él mismo.')
846
+ }
841
847
  console.log('\nNada de esto lo repone un init. Si es lo que querés: repetí con --force.')
842
848
  process.exit(1)
843
849
  }
@@ -847,6 +853,17 @@ function destroy(dir, cli) {
847
853
  }
848
854
  // El orden no es negociable: primero el wiring, después la carpeta. Al revés, cada llamada de
849
855
  // herramienta del runner queda ejecutando un guard que ya no está.
856
+ // En modo embebido la instancia **es** el repositorio: borrar la carpeta se lleva el código del
857
+ // producto, que Cauce nunca escribió y no repone nadie. Ahí se saca lo del toolkit y se deja el repo.
858
+ if (O.mode(root) === 'embedded') {
859
+ const suyo = [
860
+ 'planning', 'organization', 'teams', 'integrations', 'automatization', 'tools',
861
+ 'ops.config.json', '.cauce', 'AGENTS.md',
862
+ ]
863
+ for (const relative of suyo) fs.rmSync(path.join(root, relative), { recursive: true, force: true })
864
+ console.log(`✓ ${suyo.length} ruta(s) de Cauce quitadas de ${root}`)
865
+ return console.log(' tu repositorio queda donde está: en modo embebido la instancia era él mismo.')
866
+ }
850
867
  const adentro = process.cwd() === root || process.cwd().startsWith(`${root}${path.sep}`)
851
868
  fs.rmSync(root, { recursive: true, force: true })
852
869
  console.log(`✓ ${root} borrado`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",