@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 +62 -0
- package/automatization/workflows/autobuild.js +4 -0
- package/engine/hooks/approval.js +8 -3
- package/engine/hooks/chat.js +8 -1
- package/engine/hooks/files.js +14 -7
- package/engine/hooks/shell.js +32 -3
- package/package.json +1 -1
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
|
package/engine/hooks/approval.js
CHANGED
|
@@ -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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 }
|
package/engine/hooks/chat.js
CHANGED
|
@@ -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
|
-
|
|
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|$)/
|
package/engine/hooks/files.js
CHANGED
|
@@ -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
|
|
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 (
|
|
109
|
-
|
|
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 (
|
|
119
|
-
|
|
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 ' +
|
package/engine/hooks/shell.js
CHANGED
|
@@ -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
|
-
|
|
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
|