@ingeniomaps/cauce 0.83.0 → 0.85.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,68 @@ 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.85.0] - 2026-09-12
18
+
19
+ ### Corregido
20
+
21
+ - **Gobernanza ya no te interroga cuando sos vos quien pide el trabajo.** El guard que frena un commit que
22
+ toca reglas, ADR, `automatization/`, `engine/` o contratos de cargo te pedía **nombrar cada archivo** en
23
+ tu mensaje, o contestar «dale», o pegar las rutas en `planning/.ops-approval` — y te lo cobraba en cada
24
+ commit. Ahora, con una persona conduciendo el turno, no pregunta nada.
25
+
26
+ Lo que **no** cambia, y es el punto del guard: sigue frenando igual cuando el que commitea es un
27
+ subagente, un recorrido de Cauce o CI. Ahí nadie está conduciendo, que es exactamente para lo que existe.
28
+
29
+ Sus dos vecinos —`verify` y `dependencies`— **siguen preguntando**, y la diferencia no es quién pidió el
30
+ commit: esos frenan porque algo está mal —una verificación que falla, un manifiesto sin su lockfile— y
31
+ callarlos porque hay alguien hablando sería taparte un rojo. `OPS_GOVERNANCE_OVERRIDE` sigue existiendo
32
+ para el caso sin persona.
33
+
34
+ **Qué cambia para vos**: nada que hacer. Si venías aprobando commits de gobernanza uno por uno, o pegando
35
+ rutas a mano para poder trabajar, eso se terminó.
36
+
37
+ ## [0.84.0] - 2026-09-12
38
+
39
+ ### Corregido
40
+
41
+ - **Una frase en inglés que prohíbe ya no autoriza.** Cauce reconoce si le negás algo antes de dejarlo
42
+ pasar, y en inglés sólo entendía `don't`: «the tool doesn't read the .env» o «we can't read the .env»
43
+ nombraban el archivo, traían un verbo y ninguna negación reconocida, así que **una frase que prohíbe
44
+ terminaba autorizando**. Entran ahora las contraídas con auxiliar —`doesn't`, `isn't`, `can't`,
45
+ `won't`, `wasn't`, `wouldn't`, `shouldn't`…—, con o sin apóstrofo.
46
+
47
+ **Qué cambia para vos**: nada que hacer. Si trabajás en inglés, negar algo ahora se respeta; lo que antes
48
+ pasaba por no entenderse, se frena. Una frase como «I can't tell if the .env is right» deja de autorizar
49
+ y cuesta un «dale», que es el mismo costo que el español ya tenía.
50
+
51
+ - **La entrada de DONE dice contra qué reglas se revisó.** `autobuild` exige que Review nombre las reglas
52
+ contra las que revisó —si no las nombra, la corrida para—, pero esa lista quedaba sólo en el registro de
53
+ la corrida, que no sobrevive. Ahora va también en la línea `review=` de `DONE.md`, que es lo que queda.
54
+
55
+ **Qué cambia para vos**: nada que hacer. Al auditar una entrega vieja vas a poder decir contra qué se
56
+ revisó sin depender del registro de aquella corrida.
57
+
58
+ - **Lo que pedís vos ya no se frena sin ofrecerte ninguna salida.** Siete de los guards que pueden frenar
59
+ nunca consultaban si lo habías pedido: no tenían variable, ni línea que pegar en
60
+ `planning/.ops-approval`, ni «dale». Entre ellos estaban las reglas que cubren trabajo de todos los días
61
+ —`git reset --hard`, `git clean -f`, `git checkout -- .`, `docker compose down`, `docker system prune`—
62
+ y los que frenan escribir una credencial, un archivo generado, un snapshot del sincronizador o el motor
63
+ instalado. Pedirlos con todas las letras no servía de nada: se frenaban igual que si los hubiera
64
+ decidido el agente por su cuenta.
65
+
66
+ Ahora preguntan lo mismo que el resto de los guards: si lo pediste en el chat pasa, y si no, el bloqueo
67
+ te dice qué se frenó y un «dale» lo aprueba. De paso se cierra una asimetría que nadie había decidido:
68
+ **leer** una credencial se podía aprobar desde 0.80.0 y **escribirla** no.
69
+
70
+ **Lo que no cambia, y es a propósito**: reescribir historia publicada —`git push --force`,
71
+ `git commit --amend`—, el borrado de disco, `rm -r` sobre la raíz, el home o el directorio padre, y
72
+ `git add -A`. Ésos no se abren ni pidiéndolos, porque R8 y R23 no admiten excepción configurable.
73
+
74
+ **Qué cambia para vos**: nada que hacer ni que reinstalar. Lo que antes te obligaba a exportar una
75
+ variable para poder trabajar —o directamente no tenía salida— ahora se destraba diciéndolo. El permiso
76
+ es angosto a propósito: vale para el comando entero tal como lo escribiste, así que otra bandera es otro
77
+ comando y se vuelve a preguntar.
78
+
17
79
  ## [0.83.0] - 2026-09-11
18
80
 
19
81
  ### Agregado
@@ -790,7 +790,11 @@ while (rounds++ < MAX_TASKS) {
790
790
  if (governing.length) log(`Review contra: ${review.rules.join(', ')}`)
791
791
  // Aprobar sin declarar qué se abrió no se arregla mandando a tocar código: falló quien revisó.
792
792
  if (!review.consulted.length) return stop('review-unbacked', 'aprobó el diff sin declarar qué inspeccionó')
793
+ // Contra qué reglas se revisó va también a la entrada de DONE, y no sólo al journal: el journal muere
794
+ // con la corrida y la entrada queda, así que sin esto no había cómo reconstruir contra cuáles se
795
+ // revisó cuando alguien audita la entrega meses después (caso 122).
793
796
  reviewFact = `${review.verdict} por ${cast.review}, sobre ${review.consulted.join(', ')}`
797
+ + ((review.rules || []).length ? ` · reglas: ${review.rules.join(', ')}` : '')
794
798
  // Lo que no impide entregar no manda a tocar código, y tampoco desaparece: la mejora opinable que se
795
799
  // corrige a las apuradas cuesta una vuelta y un riesgo que nadie pidió. Va a Propuestas y no a
796
800
  // Lecciones porque lo que la revisión anotó es un cambio del producto —su evidencia es la de la
@@ -111,9 +111,14 @@ function HOW(variable, lines, input, pasteable = lines) {
111
111
  + 'sin resolver, y la aprobación compara texto, así que esa línea sólo valdría para un comando escrito '
112
112
  + 'igual. Volvé a correrlo con la ruta escrita y el bloqueo va a decir qué pegar. '
113
113
  : ''
114
- return ask + paste + unresolved
115
- + `La variable ${variable}=1 sigue existiendo y apaga el guard para toda la sesión, que es por lo que no `
116
- + 'es la vía recomendada.'
114
+ // Sin variable no se nombra ninguna. Un guard que no tiene apagado por sesión no debería anunciar uno, y
115
+ // los que estrenan salida angosta con el 117 no lo tienen a propósito: ofrecer el permiso más ancho
116
+ // cuando alcanza el más angosto es lo que hizo que la variable fuera la vía que quedaba a mano (caso 089).
117
+ const off = variable
118
+ ? `La variable ${variable}=1 sigue existiendo y apaga el guard para toda la sesión, que es por lo que no `
119
+ + 'es la vía recomendada.'
120
+ : ''
121
+ return ask + paste + unresolved + off
117
122
  }
118
123
 
119
124
  module.exports = { APPROVAL, lines, read, pending, pendingNow, where, HOW }
@@ -44,7 +44,14 @@ function load(session) {
44
44
 
45
45
  // Nombrar no es pedir: «no toques el .env» nombra el .env. Cuenta la negación que está en la misma frase y
46
46
  // antes del nombre; la coma corta, porque «leé el config, no el .env» son dos pedidos.
47
- const NEGATION = /(?:^|[^\p{L}])(?:no|nunca|jam[aá]s|ni|sin|not|never|don'?t)(?![\p{L}])/iu
47
+ // En inglés la negación se escribe contraída, y de todas se conocía una sola: `don't`. «the tool doesn't
48
+ // read the .env» nombraba el archivo, traía un verbo de la lista y ninguna negación reconocida, así que una
49
+ // frase que **prohíbe** autorizaba (caso 120). Es el error en la dirección peligrosa, al revés que el 109.
50
+ // El auxiliar entra en la alternancia con el apóstrofo opcional, que es lo que `don'?t` ya hacía para uno;
51
+ // `don't` sale de acá —`do` + `n't`—, así que no quedan dos reglas para lo mismo.
52
+ const CONTRACTED = String.raw`(?:do|does|did|is|are|was|were|ca|wo|would|should)n'?t`
53
+ const NEGATION = new RegExp(
54
+ String.raw`(?:^|[^\p{L}])(?:no|nunca|jam[aá]s|ni|sin|not|never|${CONTRACTED})(?![\p{L}])`, 'iu')
48
55
  // Un punto corta sólo si cierra la oración: el de `x.js` es parte del nombre, y cortar ahí dejaba el verbo de
49
56
  // «agregá src/x.js a .ops-approval» en otra frase que la del archivo.
50
57
  const CLAUSE = /[,;:!?\n]|\.(?=\s|$)/
@@ -76,10 +76,14 @@ function credential(input, raw) {
76
76
  : ''
77
77
  }
78
78
 
79
+ // Escribir una credencial se autoriza igual que leerla. `secrets-read` consulta la aprobación desde 0.80.0
80
+ // y éste frenaba sin ofrecer nada, así que la misma persona podía aprobar leer su `.env` y no podía aprobar
81
+ // escribirlo. La asimetría no la decidió nadie (caso 117).
79
82
  function secrets(input) {
80
83
  for (const file of filesOf(input)) {
81
84
  const reason = credential(input, file)
82
- if (reason) block(`${file} ${reason}`)
85
+ if (!reason || approved(input, file)) continue
86
+ block(`${file} ${reason}\n${AP.HOW(null, [file], input)}`)
83
87
  }
84
88
  }
85
89
 
@@ -105,9 +109,10 @@ function secretsRead(input) {
105
109
  function integrationSnapshot(input) {
106
110
  for (const raw of filesOf(input)) {
107
111
  const file = raw.replace(/\\/g, '/')
108
- if (/(?:^|\/)integrations\/[^/]+\/staging\/(?:.+\/remote\.json|sync-state\.json)$/.test(file)) {
109
- block(`${file} pertenece al sincronizador. Cura draft.md; no edites snapshots a mano.`)
110
- }
112
+ if (!/(?:^|\/)integrations\/[^/]+\/staging\/(?:.+\/remote\.json|sync-state\.json)$/.test(file)) continue
113
+ if (approved(input, raw)) continue
114
+ block(`${file} pertenece al sincronizador. Cura draft.md; no edites snapshots a mano.\n`
115
+ + AP.HOW(null, [raw], input))
111
116
  }
112
117
  }
113
118
 
@@ -115,9 +120,10 @@ function generated(input) {
115
120
  for (const raw of filesOf(input)) {
116
121
  const file = raw.replace(/\\/g, '/')
117
122
  const base = path.basename(file)
118
- if (/(?:^|[._-])generated\.[^.]+$/i.test(base) || /(?:^|[._-])gen\.(?:go|ts|js|py)$/i.test(base)) {
119
- block(`${file} parece código generado. Modifica su fuente y ejecuta el generador; no lo edites a mano.`)
120
- }
123
+ if (!/(?:^|[._-])generated\.[^.]+$/i.test(base) && !/(?:^|[._-])gen\.(?:go|ts|js|py)$/i.test(base)) continue
124
+ if (approved(input, raw)) continue
125
+ block(`${file} parece código generado. Modifica su fuente y ejecuta el generador; no lo edites a mano.\n`
126
+ + AP.HOW(null, [raw], input))
121
127
  }
122
128
  }
123
129
 
@@ -342,6 +348,7 @@ function engineWrites(input) {
342
348
  for (const raw of filesOf(input)) {
343
349
  const file = path.resolve(cwdOf(input), raw)
344
350
  if (file !== pkg && !file.startsWith(`${pkg}${path.sep}`)) continue
351
+ if (approved(input, raw)) continue
345
352
  block(`${raw} pertenece al motor de Cauce, que llega por npm.\n` +
346
353
  'Un cambio acá lo borra el próximo install y mientras tanto corrés un motor que no coincide ' +
347
354
  'con la versión que declarás. Para traer una versión nueva son dos pasos —el motor y después ' +
@@ -13,6 +13,7 @@ const {
13
13
  writableRoots, outsideRoots, DECLARE_IT, unquoted, opsRoot, withoutGitGlobals,
14
14
  } = require('./input')
15
15
  const AP = require('./approval')
16
+ const CHAT = require('./chat')
16
17
  const { publish } = require('./push')
17
18
  const { selfApprovalShell } = require('./self-approval')
18
19
  const EV = require('../core/evidence')
@@ -75,7 +76,7 @@ function destructive(input) {
75
76
  }
76
77
  publish(input, command)
77
78
  const rules = [
78
- [/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales."],
79
+ [/\bgit\s+reset\s+--hard\b/, "'git reset --hard' destruye cambios locales.", true],
79
80
  // R8 lo prohíbe sin excepción configurable y ningún guard lo miraba: `grep -rn amend engine/hooks/`
80
81
  // no devolvía una línea. Se bloquea por política y no por daño —un `--amend` sobre algo que nadie vio
81
82
  // no rompe nada—, así que el mensaje manda a lo que sí corresponde: otro commit.
@@ -83,7 +84,7 @@ function destructive(input) {
83
84
  new RegExp(String.raw`\bgit\s+commit\b${MISMO}*\s--amend\b`),
84
85
  "'git commit --amend' reescribe un commit ya creado. R8 pide uno nuevo en su lugar.",
85
86
  ],
86
- [/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento."],
87
+ [/\bgit\s+clean\s+-[^\s]*f/, "'git clean -f' borra archivos sin seguimiento.", true],
87
88
  // `git checkout -- .` destruye lo mismo que `reset --hard` y sin recuperación, pero se escribe como
88
89
  // una limpieza. Se bloquea sólo la forma ancha —`.`, `*`, `:/`, o sin ruta—: revertir un archivo
89
90
  // nombrado es trabajo corriente y no se toca.
@@ -98,14 +99,17 @@ function destructive(input) {
98
99
  + String.raw`(?:--(?=\s*(?:${COMANDO}))|(?:--\s+)?(?:\.|\*|:\/)(?=\s*(?:${COMANDO})))`),
99
100
  "'git checkout -- .' revierte todo lo no commiteado del directorio, no sólo lo que estás mirando. "
100
101
  + 'Nombrá el archivo, o commiteá lo que quieras conservar antes.',
102
+ true,
101
103
  ],
102
104
  [
103
105
  /\bdocker(?:\s+\w+)*\s+(?:volume\s+(?:rm|prune)|system\s+prune|network\s+prune)\b/,
104
106
  'La limpieza global de Docker puede borrar datos compartidos.',
107
+ true,
105
108
  ],
106
109
  [
107
110
  /\bdocker(?:\s+compose|-compose)\s+(?:\S+\s+)*(?:down|stop|kill|rm)\b/,
108
111
  'Detener un stack Compose puede interrumpir servicios compartidos.',
112
+ true,
109
113
  ],
110
114
  [
111
115
  new RegExp(ANTES + String.raw`(?:mkfs\S*|shred)\s`
@@ -117,7 +121,22 @@ function destructive(input) {
117
121
  "'rm -r' sobre /, home o el directorio padre es catastrófico.",
118
122
  ],
119
123
  ]
120
- for (const [pattern, message] of rules) if (pattern.test(command)) block(message)
124
+ // El tercer elemento dice si la regla tiene salida. Las que no la tienen son las que una persona tampoco
125
+ // debería poder abrir pidiéndolo: `--amend` y el force-push de arriba, que R8 prohíbe sin excepción
126
+ // configurable; el borrado de disco; y `rm -r` sobre raíz, home o el padre, que es la clase que gobierna
127
+ // R23. El resto no tenía salida por omisión y no por decisión: son anteriores al canal de chat de 0.81.0
128
+ // —por qué ese canal existe y qué distingue, en `chat.js`— y nadie volvió a pasarles por encima (117).
129
+ //
130
+ // El ítem que se aprueba es el comando entero tal como llegó, no el pedazo que matcheó: `git clean -f`
131
+ // no aparece entero dentro de «corré git clean -fd», así que aprobar el fragmento no destrabaría lo que
132
+ // la persona escribió. Entero es además lo que ella pegaría en el archivo, y lo más angosto: cualquier
133
+ // otra bandera es otro comando y vuelve a preguntarse.
134
+ for (const [pattern, message, open] of rules) {
135
+ if (!pattern.test(command)) continue
136
+ const item = String(raw).trim()
137
+ if (open && !AP.pending(opsRoot(input), [item], input).length) continue
138
+ block(open ? `${message}\n${AP.HOW(null, [item], input)}` : message)
139
+ }
121
140
  }
122
141
 
123
142
  function gitAdd(input) {
@@ -328,6 +347,16 @@ function governance(input) {
328
347
  if (process.env.OPS_GOVERNANCE_OVERRIDE === '1') return
329
348
  const command = commandOf(input)
330
349
  if (!isCommit(command)) return
350
+ // Con una persona conduciendo el turno, este guard no pregunta nada. Frena por **política** —qué archivos
351
+ // toca un commit— y no por un defecto de hecho, y esa pregunta a quien está dando instrucciones no le
352
+ // corresponde: lo que el guard contiene es al agente decidiendo solo (caso 126). `said` ya distingue las
353
+ // dos cosas —devuelve nada para un subagente, para un recorrido de Cauce y en CI—, así que la exención no
354
+ // alcanza a nada de eso. Es la misma forma que usa `plan-first` en `files.js`.
355
+ //
356
+ // Sus dos vecinos de gate no llevan esta exención y la diferencia no es quién pidió el commit: `verify` y
357
+ // `dependencies` frenan por algo que está mal —una verificación que falla, un manifiesto sin su lockfile—
358
+ // y callarlos porque hay alguien hablando sería tapar un rojo.
359
+ if (CHAT.said(input)) return
331
360
  // El contrato de un cargo y lo que lo mide son gobernanza, igual que un ADR o una regla. La firma de
332
361
  // «Aprobación humana» sólo estaba protegida por una frase en un prompt; `SKILL.md` y `references/`
333
362
  // son lo que la propuesta cambia, y editarlos directo saltea el ciclo entero; y `evaluations/` es el
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.83.0",
3
+ "version": "0.85.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",