@ingeniomaps/cauce 0.62.0 → 0.64.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 +59 -0
- package/engine/cli/catalog.js +9 -1
- package/engine/cli/planning.js +9 -0
- package/engine/hooks/approval.js +31 -0
- package/engine/hooks/input.js +63 -6
- package/engine/hooks/shell.js +25 -13
- package/package.json +1 -1
- package/template/AGENTS.md +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,65 @@ 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.64.0] - 2026-09-06
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **Las salidas de los guards están documentadas, y con su alcance real.** Cinco guards se pueden abrir
|
|
22
|
+
y ninguna de las cinco llaves estaba escrita en un archivo que alguien fuera a abrir: el bloqueo te
|
|
23
|
+
nombraba una variable y no había dónde leer dónde va. `AGENTS.md` ahora las lista en «Qué se puede
|
|
24
|
+
editar y qué no», con lo incómodo dicho: el guard lee la variable de **su propio proceso**, así que
|
|
25
|
+
escribirla delante del comando no llega, y la forma que sí funciona —exportarla en el entorno desde
|
|
26
|
+
el que arranca tu runner— deja el guard apagado hasta que cierres la sesión. La única con alcance de
|
|
27
|
+
operación es la aprobación de gobernanza.
|
|
28
|
+
|
|
29
|
+
- **Un commit de gobernanza se aprueba por operación, no por sesión.** El guard ofrecía como salida
|
|
30
|
+
`OPS_GOVERNANCE_OVERRIDE=1`, que se lee del entorno del proceso: prendida antes de lanzar tu runner
|
|
31
|
+
deja el guard apagado hasta que la sesión cierre. Eso convierte «aprobado este commit» en «apagado
|
|
32
|
+
hasta que me vaya», y el `Makefile` de este proyecto ya decía cuál es el alcance correcto — «la
|
|
33
|
+
autorización de R10 es por operación y humana». **Qué cambia para vos**: escribís las rutas
|
|
34
|
+
autorizadas en `planning/.governance-approval`, una por línea, y el commit pasa. Vale para ese
|
|
35
|
+
conjunto y para ningún otro: si después sumás un archivo, ese archivo no está aprobado. No se consume
|
|
36
|
+
ni se borra sola —así un commit frenado por otra razón no te obliga a rehacerla—, así que `check` te
|
|
37
|
+
avisa mientras exista para que la borres. La variable sigue funcionando y el mensaje del guard ahora
|
|
38
|
+
dice por qué no es la vía recomendada.
|
|
39
|
+
|
|
40
|
+
## [0.63.0] - 2026-09-06
|
|
41
|
+
|
|
42
|
+
### Corregido
|
|
43
|
+
|
|
44
|
+
- **El salto de línea termina la lista de argumentos de un comando.** `shell-boundary` leía los
|
|
45
|
+
argumentos de un `cp`, un `tee` o un `sed -i` cruzando a la línea siguiente, así que el destino que
|
|
46
|
+
acusaba podía ser el comando de abajo: un bloqueo que hablaba de una escritura en `…/python3`, una
|
|
47
|
+
ruta que no aparecía en el comando. Falla hacia el lado seguro —frena de más— pero señala algo que no
|
|
48
|
+
existe, y un guard que señala mal es el que se termina apagando. **Qué cambia para vos**: si escribís
|
|
49
|
+
scripts de varias líneas en un solo comando, dejás de ver bloqueos por rutas inventadas; lo que sí
|
|
50
|
+
escribe fuera de las raíces se sigue frenando igual.
|
|
51
|
+
|
|
52
|
+
- **El cuerpo de un heredoc es texto, no un comando.** Escribir un archivo con
|
|
53
|
+
`cat > nota.md <<'FIN' … FIN` juzgaba cada línea del documento como si fuera a ejecutarse, así que no
|
|
54
|
+
se podía documentar lo que los guards vigilan: un párrafo que explica por qué no se borra la raíz se
|
|
55
|
+
bloqueaba por nombrarlo, y la salida era cambiar de herramienta para escribir un archivo. Un heredoc
|
|
56
|
+
es entrada estándar y no se ejecuta nunca. **Qué cambia para vos**: el cuerpo deja de juzgarse y la
|
|
57
|
+
línea que lo abre se sigue juzgando entera, con su redirección —también si la escribís después del
|
|
58
|
+
delimitador, como en `cat <<FIN > salida`—. Lo que se pierde a cambio: un cuerpo que después alguien
|
|
59
|
+
ejecuta; al escribirse no ejecuta nada, y cuando se corra el guard verá el comando de verdad.
|
|
60
|
+
|
|
61
|
+
- **Una variable delante de `git commit` ya no apaga tres guards.** `dependencies`, `governance` y el
|
|
62
|
+
control de generados de `verify` sólo corren sobre un commit, y decidían si lo era con un ancla que
|
|
63
|
+
no contempla lo que un shell admite antes del verbo. Con `VAR=1 git commit` dejaban de correr **sin
|
|
64
|
+
decir nada**, y con la ironía de que el prefijo que se escribe para un commit de gobernanza es una
|
|
65
|
+
asignación: `OPS_GOVERNANCE_OVERRIDE=1 git commit` no leía el override, hacía que el guard no se
|
|
66
|
+
ejecutara. **Qué cambia para vos**: si venías escribiendo ese prefijo, ahora el guard corre y te va a
|
|
67
|
+
frenar. La variable se lee del entorno del guard, no del comando, y el mensaje ahora lo dice.
|
|
68
|
+
|
|
69
|
+
- **Un índice que no se puede leer deja de autorizar el commit.** `stagedFiles` devolvía una lista vacía
|
|
70
|
+
tanto si el índice estaba vacío como si no se pudo leer, y los tres guards de arriba leen esa
|
|
71
|
+
respuesta: una lectura fallida se les presentaba como «no hay nada que revisar». Llegar a una es
|
|
72
|
+
fácil, porque el guard no expande variables: `git -C $OPS commit` resuelve la ruta literal `$OPS`.
|
|
73
|
+
**Qué cambia para vos**: ese comando ahora se frena con un mensaje que nombra la causa. Escribí la
|
|
74
|
+
ruta literal en `git -C`.
|
|
75
|
+
|
|
17
76
|
## [0.62.0] - 2026-09-06
|
|
18
77
|
|
|
19
78
|
### Corregido
|
package/engine/cli/catalog.js
CHANGED
|
@@ -102,7 +102,15 @@ function evaluationBench(root, agent, caso, force, kind) {
|
|
|
102
102
|
// reintentos, rehacer un banco es una operación que falla de vez en cuando y deja la corrida sin
|
|
103
103
|
// empezar.
|
|
104
104
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 })
|
|
105
|
-
|
|
105
|
+
// Con `force`: el banco es desechable y se acaba de borrar, así que lo que sobreviva al `rmSync` se
|
|
106
|
+
// pisa en vez de cortar la corrida. Sin esto, `copyTemplate` se niega ante cualquier archivo que
|
|
107
|
+
// quede —«El destino contiene …/AGENTS.md»— y el mismo test falló así tres veces en un día, en las
|
|
108
|
+
// dos patas de la matriz. Por qué algo sobrevive a un borrado que no lanzó no está establecido.
|
|
109
|
+
//
|
|
110
|
+
// No ablanda ninguna protección: la pregunta «¿acá alguien trabajó?» la contesta el `git status` de
|
|
111
|
+
// arriba, que exige `--force` explícito para seguir. Esta segunda puerta no la eligió nadie y sólo
|
|
112
|
+
// se cerraba a veces, que es la clase de freno que enseña a re-correr sin leer.
|
|
113
|
+
IN.scaffold(dir, { name: 'Banco de evaluación', mode: 'sidecar', quiet: true, force: true })
|
|
106
114
|
// El motor por symlink: la misma resolución que en una instancia real —`node_modules/@ingeniomaps`—
|
|
107
115
|
// sin pagar un `npm install` por corrida. El cargo llega a un banco donde el CLI funciona.
|
|
108
116
|
const scope = path.join(dir, 'node_modules', '@ingeniomaps')
|
package/engine/cli/planning.js
CHANGED
|
@@ -11,6 +11,7 @@ const PC = require('../planning/contracts')
|
|
|
11
11
|
const SZ = require('../planning/sizing')
|
|
12
12
|
const ST = require('../planning/state')
|
|
13
13
|
const AD = require('../planning/adoption')
|
|
14
|
+
const AP = require('../hooks/approval')
|
|
14
15
|
const I = require('../integrations/registry')
|
|
15
16
|
const O = require('../core/ownership')
|
|
16
17
|
const OB = require('../core/onboarding')
|
|
@@ -85,6 +86,14 @@ function check(dir, cli) {
|
|
|
85
86
|
epics, milestones, done, wip, roles, humanActions: P.readHumanActions(root), adopted: new Set(adopted),
|
|
86
87
|
}))
|
|
87
88
|
warnings.push(...AD.report({ done, epics, adopted }))
|
|
89
|
+
// Una aprobación de gobernanza vale para el conjunto que nombra, así que olvidada sigue autorizando
|
|
90
|
+
// esas mismas rutas la próxima vez que alguien las stagee. No caduca sola: lo que la cierra es que se
|
|
91
|
+
// vea en cada corrida y alguien la borre.
|
|
92
|
+
const aprobadas = AP.read(path.resolve(root, '..'))
|
|
93
|
+
if (aprobadas.length) {
|
|
94
|
+
warnings.push(`planning/${AP.APPROVAL}: ${aprobadas.length} ruta(s) de gobernanza aprobadas y sin `
|
|
95
|
+
+ 'borrar; el archivo sigue autorizándolas')
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
const integration = I.validate(path.resolve(root, '..'))
|
|
90
99
|
errors.push(...integration.errors)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// La aprobación de un commit de gobernanza: una lista de rutas que una persona escribió a mano para
|
|
4
|
+
// autorizar exactamente ese cambio. Existe porque la vía que había era una variable de entorno, y una
|
|
5
|
+
// variable es por sesión: prendida antes de lanzar el runner deja el guard apagado hasta que la sesión
|
|
6
|
+
// cierre. El `Makefile` de este repositorio ya dice cuál es el alcance correcto —«la autorización de R10
|
|
7
|
+
// es por operación y humana»—, y una llave que dura toda la sesión no lo es.
|
|
8
|
+
//
|
|
9
|
+
// **Se coteja, no se consume.** Borrar el archivo al leerlo daría el mismo alcance y traería dos cosas
|
|
10
|
+
// que no queremos: hoy ningún guard escribe en el repositorio, y `governance` corre antes que `verify`,
|
|
11
|
+
// así que un commit frenado por otra razón se habría llevado puesta la aprobación y habría que
|
|
12
|
+
// rehacerla. Cotejando, la aprobación vale para el conjunto que nombra y para ningún otro: en cuanto
|
|
13
|
+
// cambia lo que está en el índice deja de servir, que es «por operación» sin fecha ni contador.
|
|
14
|
+
//
|
|
15
|
+
// Queda a la vista porque `check` avisa mientras exista. Sin eso, un archivo olvidado sigue autorizando
|
|
16
|
+
// esas mismas rutas la próxima vez que alguien las stagee, que es la puerta abierta que esto evitaba.
|
|
17
|
+
|
|
18
|
+
const path = require('node:path')
|
|
19
|
+
const fs = require('node:fs')
|
|
20
|
+
|
|
21
|
+
const APPROVAL = '.governance-approval'
|
|
22
|
+
|
|
23
|
+
// Una ruta por línea, `#` para lo demás. El archivo ausente y el vacío son lo mismo: no hay nada
|
|
24
|
+
// aprobado, que es el estado normal.
|
|
25
|
+
function read(root) {
|
|
26
|
+
let text = ''
|
|
27
|
+
try { text = fs.readFileSync(path.join(root, 'planning', APPROVAL), 'utf8') } catch { return [] }
|
|
28
|
+
return text.split('\n').map((line) => line.replace(/#.*$/, '').trim()).filter(Boolean)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { APPROVAL, read }
|
package/engine/hooks/input.js
CHANGED
|
@@ -22,10 +22,27 @@ function readInput() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// El cuerpo de un heredoc es entrada estándar: no se ejecuta, se escribe. Juzgarlo como comando frenaba
|
|
26
|
+
// documentar lo que los guards vigilan — escribir un archivo que explica por qué borrar la raíz es
|
|
27
|
+
// catastrófico se bloqueaba por nombrarlo—, y la salida era cambiar de herramienta, que es el rodeo que
|
|
28
|
+
// un guard no debería enseñar.
|
|
29
|
+
//
|
|
30
|
+
// La línea de apertura se conserva **entera**, porque sí es comando y `shell-boundary` tiene que seguir
|
|
31
|
+
// viendo dónde escribe. Entera incluye lo que va después del delimitador: `cat <<FIN > salida` es una
|
|
32
|
+
// forma válida y su destino está ahí. El cuerpo empieza en el salto de línea, no en el delimitador —
|
|
33
|
+
// recortar desde el delimitador se llevaba esa redirección, y nada lo notaba porque en la forma común
|
|
34
|
+
// el destino va antes del `<<`.
|
|
35
|
+
//
|
|
36
|
+
// Lo que se pierde: un cuerpo que después alguien ejecuta. `cat > script.sh <<EOF` no ejecuta nada al
|
|
37
|
+
// escribirse y el guard verá el comando de verdad cuando alguien corra el script; el borde filoso es
|
|
38
|
+
// `$(cat <<EOF …)`, donde el cuerpo sí corre y ya no se mira. Es el mismo trato que con el mensaje de un
|
|
39
|
+
// commit: se frena la forma habitual, no al que quiere pasar.
|
|
40
|
+
const HEREDOC = /<<(-?)\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2([^\n]*)\n[\s\S]*?^\s*\3\s*$/gm
|
|
41
|
+
|
|
25
42
|
function commandOf(input) {
|
|
26
43
|
const value = input.tool_input && (input.tool_input.command || input.tool_input.cmd)
|
|
27
44
|
|| input.command || input.input && input.input.command || process.env.OPS_HOOK_COMMAND || ''
|
|
28
|
-
return Array.isArray(value) ? value.join(' ') :
|
|
45
|
+
return String(Array.isArray(value) ? value.join(' ') : value).replace(HEREDOC, '<<$1$2$3$2$4')
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
function fileOf(input) {
|
|
@@ -86,19 +103,59 @@ function configOf(root) {
|
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
// Vacía lo que va entre comillas, dejando una marca que ningún patrón confunde con una ruta ni con un
|
|
107
|
+
// comando. Vive acá porque la usan tres lugares por razones distintas, y cada uno explica la suya donde
|
|
108
|
+
// la llama.
|
|
109
|
+
const unquoted = (command) => String(command).replace(/'[^']*'|"[^"]*"/g, '\u0000')
|
|
110
|
+
|
|
111
|
+
// Sobre qué repositorio se lee el índice. En un commit se mira el comando con el mensaje vaciado, por
|
|
112
|
+
// la misma razón por la que `destructive` lo hace: un mensaje que menciona `git -C $VAR` no está
|
|
113
|
+
// eligiendo un repositorio, lo está citando. Sin esto, el commit que explica este arreglo se bloquea a
|
|
114
|
+
// sí mismo — pasó al escribirlo.
|
|
115
|
+
//
|
|
116
|
+
// El precio es una ruta entrecomillada en el propio `-C` de un commit —`git -C "mi carpeta" commit`—,
|
|
117
|
+
// que se pierde y cae al cwd. Es más raro que un mensaje que cita un comando, y el cwd de un commit
|
|
118
|
+
// suele ser el repositorio correcto; el caso contrario deja al guard leyendo un índice ajeno.
|
|
89
119
|
function gitDirectory(command, cwd) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
120
|
+
const text = isCommit(command) ? unquoted(command) : command
|
|
121
|
+
const flag = text.match(/(?:^|\s)git\s+-C\s+(['"]?)([^\s'";&|]+)\1/)
|
|
122
|
+
const cd = text.match(/(?:^|[;&|]\s*)cd\s+(['"]?)([^\s'";&|]+)\1/)
|
|
92
123
|
return path.resolve(cwd, flag ? flag[2] : cd ? cd[2] : '.')
|
|
93
124
|
}
|
|
94
125
|
|
|
126
|
+
// Lo que un shell admite delante del verbo: asignaciones de entorno, `env` y `sudo`. `VAR=1 git commit`
|
|
127
|
+
// empieza por la asignación, así que un ancla que sólo acepta el principio del comando o un separador
|
|
128
|
+
// no ve el `git` que viene después.
|
|
129
|
+
//
|
|
130
|
+
// Falla en los dos sentidos y uno no avisa. Del lado ruidoso, el mensaje del commit vuelve a juzgarse
|
|
131
|
+
// como comando. Del silencioso —el que importa— los tres guards que sólo corren sobre un commit dejan
|
|
132
|
+
// de correr: gobernanza, dependencias y generados. Cualquier variable delante alcanza, y la ironía es
|
|
133
|
+
// que el prefijo que el procedimiento manda escribir para un commit de gobernanza es
|
|
134
|
+
// `OPS_GOVERNANCE_OVERRIDE=1`: escrito ahí, el guard no lee el override, directamente no se ejecuta.
|
|
135
|
+
const PREFIX = String.raw`(?:^|[;&|]\s*)(?:(?:env|sudo)\s+)*`
|
|
136
|
+
+ String.raw`(?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S*)\s+)*`
|
|
137
|
+
const COMMIT = new RegExp(PREFIX + String.raw`git(?:\s+-C\s+\S+)?\s+commit(?:\s|$)`)
|
|
138
|
+
|
|
95
139
|
function isCommit(command) {
|
|
96
|
-
return
|
|
140
|
+
return COMMIT.test(command)
|
|
97
141
|
}
|
|
98
142
|
|
|
143
|
+
// Un índice vacío y un índice ilegible no son la misma respuesta: la primera autoriza a seguir, la
|
|
144
|
+
// segunda no autoriza nada. Devolviendo `[]` en los dos casos, los tres guards que preguntan acá se
|
|
145
|
+
// apagaban en silencio ante cualquier lectura fallida — y llegar a una es fácil, porque `gitDirectory`
|
|
146
|
+
// no expande variables: `git -C $OPS commit` resuelve la ruta literal `$OPS`, que no existe.
|
|
147
|
+
//
|
|
148
|
+
// Es la regla que el propio shim ya tiene escrita —«Un guard que no encuentra su motor bloquea, nunca
|
|
149
|
+
// permite»—, aplicada donde faltaba. Bloquear desde acá es seguro: los tres llamadores son guards, así
|
|
150
|
+
// que no hay ningún consumidor que sólo quiera consultar el índice.
|
|
99
151
|
function stagedFiles(dir) {
|
|
100
152
|
const result = spawnSync('git', ['-C', dir, 'diff', '--cached', '--name-only'], { encoding: 'utf8' })
|
|
101
|
-
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
const why = (result.stderr || '').trim() || (result.error && result.error.message) || 'git falló'
|
|
155
|
+
block(`no se pudo leer el índice de ${dir} (${why}). Un guard que no puede verificar no autoriza. `
|
|
156
|
+
+ 'Si usaste una variable en `git -C`, escribí la ruta literal.')
|
|
157
|
+
}
|
|
158
|
+
return result.stdout.trim().split('\n').filter(Boolean)
|
|
102
159
|
}
|
|
103
160
|
|
|
104
161
|
// R10 pide «la autorización configurada para el proyecto» y `runner.allowPush` es esa configuración:
|
|
@@ -157,5 +214,5 @@ const DECLARE_IT = 'Si el proyecto necesita escribir ahí, declaralo en writable
|
|
|
157
214
|
module.exports = {
|
|
158
215
|
readInput, commandOf, patchOf, filesOf, contentOf, cwdOf, block, configOf,
|
|
159
216
|
gitDirectory, isCommit, stagedFiles, pushAllowed, findOpsRoot,
|
|
160
|
-
writableRoots, outsideRoots, DECLARE_IT,
|
|
217
|
+
writableRoots, outsideRoots, DECLARE_IT, unquoted,
|
|
161
218
|
}
|
package/engine/hooks/shell.js
CHANGED
|
@@ -10,8 +10,9 @@ const path = require('node:path')
|
|
|
10
10
|
const { spawnSync } = require('node:child_process')
|
|
11
11
|
const {
|
|
12
12
|
commandOf, cwdOf, block, gitDirectory, isCommit, stagedFiles, pushAllowed,
|
|
13
|
-
writableRoots, outsideRoots, DECLARE_IT,
|
|
13
|
+
writableRoots, outsideRoots, DECLARE_IT, unquoted, findOpsRoot,
|
|
14
14
|
} = require('./input')
|
|
15
|
+
const AP = require('./approval')
|
|
15
16
|
|
|
16
17
|
// Dónde empieza y dónde termina una palabra dentro de un comando. Tres reglas de la tabla de abajo lo
|
|
17
18
|
// decidían por su cuenta admitiendo sólo un espacio, el principio o el fin, y en un shell una palabra
|
|
@@ -169,10 +170,17 @@ function dependencies(input) {
|
|
|
169
170
|
// Tres familias, porque los comandos no nombran su destino igual: `tee` y `truncate` escriben en cada
|
|
170
171
|
// argumento, `cp` y sus hermanos en el último, y `sed` sólo escribe con `-i` —sin él lee y manda a
|
|
171
172
|
// stdout, y esa redirección la ve REDIRECT—.
|
|
173
|
+
//
|
|
174
|
+
// El salto de línea termina una lista de argumentos igual que `;`. Sin excluirlo, la de un `cp` seguía
|
|
175
|
+
// leyendo la línea de abajo y el destino terminaba siendo el comando siguiente: un bloqueo que nombraba
|
|
176
|
+
// `…/python3`, una ruta que no aparecía en el comando. Se veía con un heredoc debajo, pero el heredoc no
|
|
177
|
+
// era la causa —sólo hacía que la lectura frenara en un token que sobrevive—: sin él la lista cruzaba
|
|
178
|
+
// igual y el último token era la marca de lo entrecomillado, que el filtro final descarta. O sea que
|
|
179
|
+
// pasaba de casualidad, y aserciar que pasa no fijaba nada.
|
|
172
180
|
const REDIRECT = /(?:^|[\s(])&?\d*>>?\s*(?![&(])([^\s;|&<>()]+)/g
|
|
173
|
-
const EVERY_ARG = /(?:^|[\s;|&(])(tee|truncate)\s+([^;|&<>()]+)/g
|
|
174
|
-
const LAST_ARG = /(?:^|[\s;|&(])(cp|mv|install|rsync)\s+([^;|&<>()]+)/g
|
|
175
|
-
const SED = /(?:^|[\s;|&(])sed\s+([^;|&<>()]+)/g
|
|
181
|
+
const EVERY_ARG = /(?:^|[\s;|&(])(tee|truncate)\s+([^;|&<>()\n]+)/g
|
|
182
|
+
const LAST_ARG = /(?:^|[\s;|&(])(cp|mv|install|rsync)\s+([^;|&<>()\n]+)/g
|
|
183
|
+
const SED = /(?:^|[\s;|&(])sed\s+([^;|&<>()\n]+)/g
|
|
176
184
|
const IN_PLACE = /(?:^|\s)-{1,2}i/
|
|
177
185
|
|
|
178
186
|
// Los argumentos que no son flags. El valor de un flag se cuela —`truncate -s 0 log` trae el `0`— y no
|
|
@@ -180,10 +188,6 @@ const IN_PLACE = /(?:^|\s)-{1,2}i/
|
|
|
180
188
|
// decide un bloqueo. Filtrarlo sería una rama que ninguna prueba puede ver caer.
|
|
181
189
|
const positional = (text) => text.trim().split(/\s+/).filter((one) => one && !one.startsWith('-'))
|
|
182
190
|
|
|
183
|
-
// Vacía lo que va entre comillas, dejando una marca que ningún patrón confunde con una ruta ni con un
|
|
184
|
-
// comando. Lo usan dos guards por razones distintas, y cada uno explica la suya donde lo llama.
|
|
185
|
-
const unquoted = (command) => String(command).replace(/'[^']*'|"[^"]*"/g, '\u0000')
|
|
186
|
-
|
|
187
191
|
// Un `>` adentro de una cadena no redirige nada. Pierde el destino entrecomillado, que es un falso
|
|
188
192
|
// negativo — el error barato en un guard que ya es incompleto, porque el caro es frenar un comando
|
|
189
193
|
// legítimo y que alguien apague el guard entero.
|
|
@@ -247,11 +251,19 @@ function governance(input) {
|
|
|
247
251
|
String.raw`|evaluations\/(?:cases\/|expected-behaviors\.yaml)|learning\/proposals\/))`,
|
|
248
252
|
)
|
|
249
253
|
const governed = stagedFiles(dir).filter((file) => governedPattern.test(file))
|
|
250
|
-
if (governed.length)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
254
|
+
if (!governed.length) return
|
|
255
|
+
// La aprobación vale para lo que nombra y para nada más: lo que quede sin cubrir es lo que se
|
|
256
|
+
// reporta. Así una aprobación vieja no autoriza el archivo que se sumó después, que es la diferencia
|
|
257
|
+
// entre una llave por operación y una puerta que quedó abierta.
|
|
258
|
+
const root = findOpsRoot(process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || cwdOf(input))
|
|
259
|
+
const aprobados = new Set(root ? AP.read(root) : [])
|
|
260
|
+
const pendientes = governed.filter((file) => !aprobados.has(file))
|
|
261
|
+
if (!pendientes.length) return
|
|
262
|
+
const files = pendientes.map((file) => ` - ${file}`).join('\n')
|
|
263
|
+
block(`El commit toca gobernanza protegida:\n${files}\n`
|
|
264
|
+
+ `Aprobalo escribiendo esas rutas en planning/${AP.APPROVAL}, una por línea: vale para ese `
|
|
265
|
+
+ 'conjunto y deja de valer en cuanto cambie. La variable OPS_GOVERNANCE_OVERRIDE=1 sigue '
|
|
266
|
+
+ 'existiendo y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.')
|
|
255
267
|
}
|
|
256
268
|
|
|
257
269
|
function run(program, args, cwd) {
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -98,6 +98,34 @@ siquiera están acá — los lee el motor desde el paquete.
|
|
|
98
98
|
Un guard existente **no se edita**: `upgrade` detecta el cambio y se detiene antes de pisarlo, y con
|
|
99
99
|
`--force` deja registrado qué descartó.
|
|
100
100
|
|
|
101
|
+
### Cuando un guard te frena con razón
|
|
102
|
+
|
|
103
|
+
Algunos bloqueos tienen salida, y conviene saber cuál antes de necesitarla — el momento en que un guard
|
|
104
|
+
te frena es el peor para elegir bien.
|
|
105
|
+
|
|
106
|
+
**Un commit que toca gobernanza** —reglas, ADRs, el contrato de un cargo o lo que lo mide— se autoriza
|
|
107
|
+
escribiendo las rutas en `planning/.governance-approval`, una por línea, con `#` para lo que no sea una
|
|
108
|
+
ruta. Vale para ese conjunto y para ningún otro: si después sumás un archivo, ese archivo no está
|
|
109
|
+
aprobado y el guard lo nombra. No se borra sola —así un commit frenado por otra cosa no te obliga a
|
|
110
|
+
rehacerla—, así que `check` te avisa mientras exista, y borrarla es parte de terminar.
|
|
111
|
+
|
|
112
|
+
**Las demás salidas son variables de entorno**, y hay que decir su alcance
|
|
113
|
+
porque no es el que uno espera: el guard la lee de **su propio proceso**, no del comando. Escribirla
|
|
114
|
+
delante —`VAR=1 git commit`— no llega. La forma que sí funciona es exportarla en el entorno desde el que
|
|
115
|
+
arranca tu runner, y eso deja el guard apagado **hasta que cierres la sesión**, no para un comando.
|
|
116
|
+
|
|
117
|
+
| variable | qué abre |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `OPS_GOVERNANCE_OVERRIDE=1` | lo mismo que la aprobación de arriba, pero para toda la sesión |
|
|
120
|
+
| `OPS_MIGRATIONS_OVERRIDE=1` | escribir SQL destructivo en una migración |
|
|
121
|
+
| `OPS_TEST_EVIDENCE_OVERRIDE=1` | borrar o apagar una prueba |
|
|
122
|
+
| `OPS_DEPENDENCIES_OVERRIDE=1` | tocar manifiestos y lockfiles, publicar o instalar global |
|
|
123
|
+
| `OPS_SKIP_VERIFY=1` | saltear los gates del stack antes de un commit |
|
|
124
|
+
|
|
125
|
+
Son de sesión y no de operación, que es exactamente lo que la aprobación de gobernanza vino a corregir.
|
|
126
|
+
Mientras sigan así, lo que corresponde es prenderlas para lo que hacía falta y apagarlas después — y que
|
|
127
|
+
la razón quede escrita donde alguien la lea, no sólo en la memoria de quien la prendió.
|
|
128
|
+
|
|
101
129
|
## Cómo leer el estado
|
|
102
130
|
|
|
103
131
|
Antes de abrir un archivo de `planning/`, preguntarle al CLI: es determinista, no gasta contexto y no
|