@ingeniomaps/cauce 0.38.0 → 0.40.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,32 @@ 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.40.0] - 2026-08-19
18
+
19
+ ### Corregido
20
+
21
+ - **En modo embebido, Codex se quedaba sin instrucciones.** `AGENTS.md` es el único nombre que el runner
22
+ y la instancia comparten, y ahí `install` conservaba el archivo entero —lo correcto para algo del
23
+ proyecto—, así que el recorrido que ese runner tenía que seguir nunca llegaba a estar escrito. Ahora su
24
+ contenido se fusiona adentro, entre marcas: reinstalar reemplaza el bloque en vez de duplicarlo,
25
+ desinstalar lo saca sin llevarse el archivo, y todo lo que la empresa escribió alrededor queda intacto.
26
+
27
+ `doctor` lo tenía al revés en ese archivo: comparando el hash completo avisaba cuando el bloque estaba
28
+ bien —el texto de la empresa alrededor difiere siempre— y decía «operativo» cuando alguien lo había
29
+ borrado, que es la falla que importa.
30
+
31
+ ## [0.39.0] - 2026-08-19
32
+
33
+ ### Corregido
34
+
35
+ - **`check` nombra las credenciales que nadie se llevó.** El arranque tiene que dejar una fila por cada
36
+ variable que el proyecto declara, y en cuatro corridas reales se cumplió en proporción a lo que se
37
+ habló: las que sólo estaban en el inventario quedaron afuera —entre ellas el broker por donde entran
38
+ los datos y el servicio al que se le mandan errores—. Ahora `check` nombra las que no aparecen ni en el
39
+ mapa ni en `HUMAN_ACTIONS.md`. Mira sólo cuando la instancia ya tiene contexto escrito, porque antes no
40
+ hay dónde tendrían que estar, y avisa en vez de fallar: una variable sin dueño no rompe nada hoy, rompe
41
+ el día que alguien tiene que desplegar.
42
+
17
43
  ## [0.38.0] - 2026-08-19
18
44
 
19
45
  ### Añadido
@@ -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
@@ -465,6 +465,31 @@ function seccionesPerdidas(root) {
465
465
  return avisos
466
466
  }
467
467
 
468
+ // Credenciales que el proyecto declara y que no aparecen en ningún contrato. El arranque tiene que
469
+ // dejar una fila por cada una —quién la carga y dónde— y en la práctica cubre las que se hablaron en la
470
+ // conversación: las que sólo estaban en el inventario se pierden, y con ellas el servicio externo que
471
+ // hay detrás. Una variable sin dueño no rompe nada hoy; rompe el día que alguien tiene que desplegar.
472
+ //
473
+ // Sólo cuando la instancia ya tiene contexto escrito: antes del arranque no hay dónde estuvieran.
474
+ function credencialesSinDueño(root) {
475
+ if (OB.guide(root).fresh) return []
476
+ const contratos = ['AGENTS.md', path.join('planning', 'HUMAN_ACTIONS.md')]
477
+ .map((file) => { try { return fs.readFileSync(path.join(root, file), 'utf8') } catch { return '' } })
478
+ .join('\n')
479
+ if (!contratos) return []
480
+ const huerfanas = []
481
+ for (const service of inventory(root)) {
482
+ for (const nombre of (service.env || {}).names || []) {
483
+ if (!contratos.includes(nombre)) huerfanas.push(`${nombre} (${service.path})`)
484
+ }
485
+ }
486
+ if (!huerfanas.length) return []
487
+ const lista = huerfanas.length > 4
488
+ ? `${huerfanas.slice(0, 4).join(', ')} y ${huerfanas.length - 4} más`
489
+ : huerfanas.join(', ')
490
+ return [`el proyecto declara ${lista} y no aparecen en el mapa ni en HUMAN_ACTIONS: nadie las carga`]
491
+ }
492
+
468
493
  function check(dir, cli) {
469
494
  const root = path.resolve(dir || '.')
470
495
  const errors = []
@@ -578,6 +603,7 @@ function check(dir, cli) {
578
603
  for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
579
604
 
580
605
  warnings.push(...seccionesPerdidas(path.resolve(root, '..')))
606
+ warnings.push(...credencialesSinDueño(path.resolve(root, '..')))
581
607
 
582
608
  if (cli.has('--json')) {
583
609
  console.log(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",