@ingeniomaps/cauce 0.79.0 → 0.81.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +6 -3
  3. package/automatization/hooks/README.md +48 -2
  4. package/automatization/hooks/guard-chat.sh +5 -0
  5. package/automatization/hooks/guard-secrets-read.sh +3 -0
  6. package/automatization/hooks/guard-secrets-shell.sh +3 -0
  7. package/automatization/runners/claude/README.md +5 -2
  8. package/automatization/runners/claude/manifest.json +26 -1
  9. package/automatization/runners/claude/settings.json +13 -0
  10. package/automatization/runners/codex/README.md +4 -0
  11. package/automatization/runners/codex/hooks.json +7 -0
  12. package/automatization/runners/gemini/README.md +6 -2
  13. package/automatization/runners/gemini/settings.json +19 -0
  14. package/engine/automation/index.js +9 -1
  15. package/engine/cli/args.js +1 -0
  16. package/engine/cli/bootstrap.js +1 -1
  17. package/engine/cli/ops.js +9 -5
  18. package/engine/cli/wiring.js +21 -3
  19. package/engine/config/paths.js +5 -3
  20. package/engine/hooks/approval.js +42 -8
  21. package/engine/hooks/chat.js +122 -0
  22. package/engine/hooks/files.js +94 -35
  23. package/engine/hooks/input.js +7 -1
  24. package/engine/hooks/run.js +30 -3
  25. package/engine/hooks/secrets-shell.js +62 -0
  26. package/engine/hooks/self-approval.js +31 -0
  27. package/engine/hooks/shell.js +49 -28
  28. package/engine/integrations/providers/jira.js +1 -1
  29. package/engine/integrations/registry.js +28 -5
  30. package/engine/secrets/index.js +201 -0
  31. package/package.json +1 -1
  32. package/template/AGENTS.md +14 -3
  33. package/template/integrations/README.md +21 -0
  34. package/template/organization/README.md +52 -0
  35. package/template/planning/adr/README.md +1 -0
  36. package/template/planning/adr/system/OPS-007-contrato-de-secretos-compartido.md +62 -0
package/CHANGELOG.md CHANGED
@@ -14,6 +14,138 @@ 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.81.0] - 2026-09-11
18
+
19
+ ### Cambiado
20
+
21
+ - **Lo que pedís en el chat ya no te frena.** Los guards veían la herramienta y nada de la conversación,
22
+ así que frenaban igual lo que pediste con todas las letras y lo que el agente decidía solo. Ahora, en
23
+ Claude Code, Codex y Gemini, un hook registra tu mensaje y los guards lo leen: si nombraste lo que se
24
+ iba a frenar —«borrá la prueba de altas», «reescribí la migración 004»—, pasa; si no, el agente te dice
25
+ qué se frenó y un «dale» aprueba exactamente eso. `plan-first` deja de pedir un plan cuando el cambio lo
26
+ pediste vos. Lo que el agente hace por su cuenta, dentro de un recorrido o en un subagente, se sigue
27
+ frenando igual.
28
+
29
+ **Qué cambia para vos**: corré `automation install` para que tu runner registre el hook nuevo
30
+ (`UserPromptSubmit` en Claude y Codex, `BeforeAgent` en Gemini); en Codex, confialo con `/hooks`. Tu
31
+ mensaje se guarda en el temporal del sistema, no en el repositorio, y sólo el último de cada sesión.
32
+ Antigravity sigue con el archivo de aprobación.
33
+
34
+ - **Leer una credencial por shell se frena en los cuatro runners, y en Claude ya no te frena a vos.** Un
35
+ guard nuevo, `secrets-shell`, frena el comando que muestra una credencial —`cat .env`, `head`, `grep`,
36
+ `source`, una redirección `<`, un `node -e`— en Claude, Codex, Gemini y Antigravity. Hasta ahora sólo
37
+ Claude lo frenaba, con reglas nativas `permissions.deny` que tampoco dejaban pasar tu pedido; esas reglas
38
+ se retiran, y como con el resto de los guards, si lo pedís en el chat, pasa.
39
+
40
+ **Qué cambia para vos**: corré `automation install`; en Claude quita las reglas que Cauce había puesto y
41
+ conserva las tuyas. Si querés un bloqueo nativo total, escribilo como regla propia.
42
+
43
+ ### Corregido
44
+
45
+ - **El bloqueo de `verify` cita la prueba que falló también con jest, vitest, mocha y pytest.** Con esas
46
+ herramientas citaba el archivo, el resumen o —con `pytest -v`— una prueba verde cuyo nombre decía «error».
47
+ Ahora reconoce cómo marca cada una la prueba que falla, comprobado contra la salida real de cada una.
48
+
49
+ **Qué cambia para vos**: el mensaje apunta a la prueba que hay que mirar.
50
+
51
+ - **El agente ya no puede escribirse la aprobación.** `planning/.ops-approval` se escribía con la misma
52
+ herramienta que usa el agente y ningún guard lo miraba, así que una aprobación suya destrababa igual que
53
+ una tuya. Los guards de límites lo frenan ahora —por la herramienta de escritura y por el destino evidente
54
+ de un comando—, salvo que se lo hayas pedido nombrándolo en el chat.
55
+
56
+ **Qué cambia para vos**: nada si el archivo lo editás vos. Si el agente lo escribía por pedido tuyo,
57
+ ahora se lo pedís en el chat.
58
+
59
+ - **En sidecar, el bloqueo dice en qué archivo aprobar.** Mandaba a `planning/.ops-approval`, y desde la
60
+ carpeta del workspace, donde se abre la sesión, ése no es el `planning/` de la instancia: pegar donde
61
+ decía no destrababa nada. Ahora nombra la ruta que el guard lee —`acme-ops/planning/.ops-approval`—; en
62
+ modo embebido sigue diciendo `planning/.ops-approval`.
63
+
64
+ **Qué cambia para vos**: nada que hacer; el mensaje dice dónde.
65
+
66
+ ## [0.80.0] - 2026-09-10
67
+
68
+ ### Agregado
69
+
70
+ - **Leer una credencial también se frena, y las identidades declaradas cuentan como credencial.** Un guard
71
+ nuevo, `secrets-read`, corre en la herramienta de lectura de Claude (`Read`) y de Gemini (`read_file`) y
72
+ frena los mismos archivos que `secrets` frena al escribir. Esos archivos ahora incluyen las identidades
73
+ `source: file` de `organization/secrets.json`, que antes pasaban por no tener nombre de credencial. En
74
+ Claude, además, la instalación agrega reglas `permissions.deny` `Read(...)` que el propio Claude aplica
75
+ también a `cat`, `head`, `tail`, `sed` y redirecciones.
76
+
77
+ **Qué cambia para vos**: el agente deja de poder leer `.env`, claves y tokens conocidos; `.env.example`
78
+ sigue legible. Si una lectura hace falta, aprobá la ruta en `planning/.ops-approval`. Corré
79
+ `automation install` para que tu `.claude/settings.json` reciba las reglas: se suman a las tuyas. No es un
80
+ límite de seguridad —un `grep -r` o un script propio siguen leyendo—, y en Codex y Antigravity no hay guard
81
+ de lectura.
82
+
83
+ - **Un proveedor de integraciones propio, sin tocar Cauce.** El registro de `integrations/config.json`
84
+ ya tenía un campo `adapter` que nadie leía: ahora `"adapter": "./adapter.js"` carga el adaptador de la
85
+ empresa desde `integrations/<nombre>/`, e `integration enable` lo conecta sin exigir un molde de Cauce.
86
+ El adaptador declara `contract: 1` y las tres funciones del contrato, y `check` rechaza el que no
87
+ cumpla.
88
+
89
+ **Qué cambia para vos**: nada si sólo usás Jira. Para otra herramienta, `integrations/README.md` de tu
90
+ instancia trae el recorrido.
91
+
92
+ - **Un contrato de secretos compartido entre repositorios, y un chequeo sin red.** Si tus servicios
93
+ usan un gestor de secretos, los scripts y workflows que lo conectan terminan copiados en cada
94
+ repositorio, y el arreglo que alguien hizo en uno no llega a los demás. `organization/secrets.json`
95
+ declara qué cuenta, qué identidad y qué archivos comparte cada servicio, y
96
+ `node tools/ops.js secrets check .` compara cada copia contra la canónica que guarda la instancia:
97
+ dice cuál quedó atrás y el `cp` que la pone al día. También falla si una credencial vive dentro de un
98
+ repositorio o si la declaración guarda un valor en vez de una referencia.
99
+
100
+ **Qué cambia para vos**: nada si no lo usás. Para adoptarlo, `organization/README.md` trae el formato
101
+ y el recorrido. Cauce no se conecta a ningún gestor ni trae adaptadores: lo que habla con el gestor
102
+ sigue siendo tuyo. Si tus proyectos tienen instancias separadas, el chequeo sólo compara dentro de
103
+ cada una: para que un esqueleto y sus derivados se midan contra lo mismo, van como raíces de la misma
104
+ instancia.
105
+
106
+ ### Corregido
107
+
108
+ - **`init` rechaza un runner mal escrito sin crear la instancia.** Con `--runner none` —el valor es
109
+ `ninguno`— o una `--integration` que no existe, `init` escribía la instancia entera y recién después
110
+ salía con error: el código de salida decía que no había pasado nada y el segundo intento encontraba la
111
+ carpeta creada. Ahora valida los dos valores antes de escribir.
112
+
113
+ **Qué cambia para vos**: nada con un valor correcto. Con uno mal escrito, el destino queda como estaba.
114
+
115
+ - **El bloqueo de `verify` cita la prueba que falló.** Mostraba la primera línea de la salida que dijera
116
+ «error», y en un reporte de pruebas ésa puede ser una verde cuyo nombre lo dice. Ahora, con la salida de
117
+ `node --test` —spec y TAP— y de `go test`, cita la primera prueba en rojo; con otras herramientas sigue
118
+ buscando por palabra, pero ya no elige una línea marcada como verde.
119
+
120
+ **Qué cambia para vos**: el mensaje apunta a la prueba que hay que mirar.
121
+
122
+ - **Cada bloqueo con salida angosta dice qué líneas pegar.** Aprobar en `planning/.ops-approval` sólo
123
+ funciona si la ruta está escrita en la forma que ese guard coteja —absoluta la de un `Write`, relativa
124
+ al repositorio la de un commit—, y el mensaje decía «escribí esa(s) ruta(s)» sin nombrarlas. `verify`
125
+ y `dependencies` ni siquiera mostraban la línea. Ahora cada bloqueo imprime las líneas exactas, y
126
+ pegarlas tal cual destraba ese mismo bloqueo.
127
+
128
+ **Qué cambia para vos**: si una aprobación no pegaba y terminabas exportando la variable del guard,
129
+ pegá lo que dice el mensaje.
130
+
131
+ - **`plan-first` deja de frenar la configuración y los archivos de la instancia.** Frenaba
132
+ `ops.config.json` —justo el archivo que el límite de raíces manda a editar para declarar una ruta— y,
133
+ en sidecar, también `AGENTS.md`, `CLAUDE.md`, `package.json` y `.gitignore` de la instancia, como si
134
+ fueran producto. Ahora producto es el código de una raíz declarada: la instancia sidecar no lo es
135
+ aunque viva dentro de su raíz (`..`), y lo que queda fuera de toda raíz —lo declarado en
136
+ `writableOutsideRoots`— tampoco. En embedded, el `package.json` de la raíz sigue siendo producto.
137
+
138
+ **Qué cambia para vos**: si aprobabas esas rutas a mano en `.ops-approval` o exportabas
139
+ `OPS_PLAN_FIRST_OVERRIDE` para poder tocarlas, ya no hace falta.
140
+
141
+ - **`verify` deja afuera del índice de su copia lo que enlaza.** Cuando el árbol difiere del índice, el
142
+ guard corre los gates sobre una copia del índice con lo ignorado enlazado —`node_modules`, `.env`—. Si
143
+ tu `.gitignore` escribe ese directorio con barra final (`node_modules/`), el enlace entraba al índice de
144
+ la copia, y un gate que recorre lo trackeado —`git ls-files`— lo recibía como si fuera parte del commit.
145
+
146
+ **Qué cambia para vos**: si una suite que pasa a mano fallaba bajo `verify` sólo cuando tenías archivos
147
+ sin trackear, podía ser esto.
148
+
17
149
  ## [0.79.0] - 2026-09-10
18
150
 
19
151
  ### Corregido
package/README.md CHANGED
@@ -216,6 +216,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
216
216
  | `ops integration rebase <ops-root> jira KEY` | Recalcula el borrador canónico sin avanzar la base remota. |
217
217
  | `ops integration reconcile <ops-root> jira KEY` | Conserva curación sobre la nueva base remota. |
218
218
  | `ops integration writeback-plan <ops-root> jira` | Muestra escrituras posibles sin ejecutarlas. |
219
+ | `ops secrets check <ops-root>` | Compara el contrato de secretos compartido contra cada servicio, sin conectarse. |
219
220
  | `ops automation list <ops-root>` | Lista adaptadores y su instalación. |
220
221
  | `ops automation list-hooks <ops-root>` | Describe los guards portables disponibles. |
221
222
  | `ops automation check <ops-root>` | Valida guards, permisos y configuraciones. |
@@ -314,9 +315,11 @@ La sincronización solo lee Jira. `writeback-plan` calcula intención local, no
314
315
  `writeBack` permanece en `false`. Consulta el [recorrido de Jira](template/integrations/jira/README.md) antes
315
316
  de conectar una instancia real.
316
317
 
317
- Para añadir otra herramienta se crea un adaptador en `engine/integrations/providers/` con `validateConfig`,
318
- `fetchItems` y `normalizeFixture`, y se registra en `engine/integrations/registry.js`. Staging, revisión,
319
- promoción y validación no se reimplementan. Consulta [integrations/README.md](integrations/README.md).
318
+ Para añadir otra herramienta no hace falta tocar Cauce: el adaptador se escribe en la instancia, se
319
+ registra con una ruta en el campo `adapter` y cumple el contrato —`contract: 1`, `validateConfig`,
320
+ `fetchItems` y `normalizeFixture`—. Staging, revisión, promoción y validación no se reimplementan. El
321
+ recorrido está en [template/integrations/README.md](template/integrations/README.md) y el contrato en
322
+ [integrations/README.md](integrations/README.md).
320
323
 
321
324
  ## Hooks y runners
322
325
 
@@ -4,6 +4,7 @@ Los hooks convierten invariantes comprobables en gates mecánicos. La base recom
4
4
 
5
5
  - comandos destructivos, force, amend y stage indiscriminado;
6
6
  - escritura de secretos o credenciales;
7
+ - lectura de credenciales conocidas o declaradas, en los runners que tienen una herramienta de lectura;
7
8
  - edición manual de código generado y drift respecto a OpenAPI/SQL;
8
9
  - commits sin Verify aplicable;
9
10
  - apagado o borrado de la prueba que juzga el cambio;
@@ -44,6 +45,46 @@ publica— por la sensación de que ya está cubierto.
44
45
  Regla práctica: si algo **tiene** que ser imposible, no lo pongas acá. Ponelo donde no dependa de leer
45
46
  una cadena — permisos del runner, alcance del token, aprobación de un PR.
46
47
 
48
+ ### Leer una credencial
49
+
50
+ Dos guards frenan leer un archivo que `secrets` frenaría al escribir —los nombres conocidos y las
51
+ identidades que declara `organization/secrets.json`—: `secrets-read` en las herramientas de lectura y de
52
+ búsqueda del runner (`Read` y `Grep` en Claude, `read_file` y `grep_search` en Gemini) y `secrets-shell` en
53
+ el shell de los cuatro runners, cuando un comando la muestra —`cat`, `head`, `grep`, `sed`, `source`, una
54
+ redirección `<`, un intérprete en línea—. Un comodín que la nombra —`rg -g '.env*'`, `--include='*.env'`—
55
+ cuenta igual. Lo que sólo la nombra —`ls`, `test -f`, `rm`, `cp .env.example .env`— pasa, y lo que la
56
+ persona pidió en el chat también.
57
+
58
+ Lo que no ve ninguno, comprobado en sesiones reales: una búsqueda sobre la carpeta que no nombra el archivo
59
+ —un `rg` de todo el árbol—, un nombre armado en una variable, y en Gemini el propio entorno del runner,
60
+ que carga el `.env` de la carpeta al arrancar (documentado en geminicli.com/docs/reference/configuration):
61
+ un `env` muestra sus valores sin leer ningún archivo.
62
+
63
+ Hasta 0.80.0 Claude traía además reglas nativas `permissions.deny` `Read(...)`. Las aplica Claude mismo, sin
64
+ pasar por ningún hook, así que frenaban también lo que la persona pedía, mientras en los otros runners el
65
+ shell no tenía freno (caso 104). `automation install` las retira de una instalación anterior y conserva las
66
+ que escribió la empresa: quien quiera un bloqueo nativo total lo escribe como regla propia.
67
+
68
+ ### Lo que pide la persona
69
+
70
+ Un guard ve la llamada a la herramienta y nada de la conversación, así que frenaba igual lo que la persona
71
+ pidió con todas las letras y lo que el agente decidió solo. `chat` corre sobre el mensaje de la persona —el
72
+ runner lo dispara cuando ella manda algo, nunca por el resultado de una herramienta— y lo deja donde los
73
+ guards lo leen:
74
+
75
+ - lo que la persona **nombró** en su mensaje pasa: «leé el `.env`» autoriza leer el `.env`, y «no toques
76
+ el `.env`» no;
77
+ - lo que se frenó sin que lo nombrara queda anotado, y un «dale» en el mensaje siguiente aprueba
78
+ exactamente eso;
79
+ - `plan-first` no aplica: el plan es del trabajo que va por tareas.
80
+
81
+ No cuenta cuando no hay persona —CI, o un aviso del runner como el de un subagente que terminó—, cuando lo
82
+ que pidió es un recorrido de Cauce (`/autobuild`, `$flow`…), ni en la llamada de un subagente, que Claude
83
+ marca con `agent_id`. En Claude y Codex cada llamada trae el identificador del mensaje que la originó;
84
+ Gemini no lo manda, y ahí vale el último mensaje. El registro vive en el temporal del sistema, uno por
85
+ sesión, y los guards de límites lo cuidan junto con `planning/.ops-approval`: el agente no puede
86
+ escribirse ninguno de los dos. Como todo lo de esta página, frena la forma habitual y no un script decidido.
87
+
47
88
  ## Cómo se ejecutan
48
89
 
49
90
  ```text
@@ -67,10 +108,15 @@ runner, mientras la lógica se prueba y mantiene una sola vez en `engine/hooks/r
67
108
 
68
109
  | Grupo | Guards | Wrapper |
69
110
  |---|---|---|
70
- | `pre-shell` | destructive, git-add, dependencies, governance, verify | `guard-shell.sh` |
71
- | `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence | `guard-files.sh` |
111
+ | `pre-shell` | destructive, git-add, dependencies, governance, verify, shell-boundary | `guard-shell.sh` |
112
+ | `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence, plan-first | `guard-files.sh` |
113
+ | `pre-read` | secrets-read | `guard-secrets-read.sh` |
114
+ | `prompt` | chat | `guard-chat.sh` |
72
115
  | `stop` | planning-drift | `guard-planning-drift.sh` |
73
116
 
117
+ `prompt` corre sobre el mensaje de la persona —`UserPromptSubmit` en Claude y Codex, `BeforeAgent` en
118
+ Gemini— y no sobre una herramienta, y su shim sale siempre con 0.
119
+
74
120
  Registrar el grupo gasta un proceso por herramienta en lugar de cinco, con el mismo orden y la misma
75
121
  semántica: el primer guard que bloquea corta la ejecución. Un runner que necesite granularidad fina puede
76
122
  seguir registrando los wrappers individuales.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Shim: qué registra está en engine/hooks/run.js → guards['chat']. Sale siempre con 0: el runner lo corre
3
+ # sobre el mensaje de la persona, y un 2 ahí no frena una herramienta sino lo que la persona escribió.
4
+ "$(dirname "$0")/run-hook.sh" chat >/dev/null 2>&1
5
+ exit 0
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ # Shim: qué bloquea el guard está en engine/hooks/run.js → guards['secrets-read'].
3
+ exec "$(dirname "$0")/run-hook.sh" secrets-read
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ # Shim: qué bloquea el guard está en engine/hooks/run.js → guards['secrets-shell'].
3
+ exec "$(dirname "$0")/run-hook.sh" secrets-shell
@@ -1,12 +1,15 @@
1
1
  # Claude Code
2
2
 
3
- Adaptador nativo mediante `PreToolUse` y `Stop`. Instalar con:
3
+ Adaptador nativo mediante `PreToolUse`, `UserPromptSubmit` y `Stop`. Instalar con:
4
4
 
5
5
  ```bash
6
6
  node tools/ops.js automation install . claude
7
7
  ```
8
8
 
9
- El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves. Si ya
9
+ El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves. Las reglas
10
+ `permissions.deny` `Read(...)` que Cauce sumaba hasta 0.80.0 las retira al reinstalar —las declara
11
+ `config.retired` en `manifest.json`— y deja las que el proyecto haya escrito: leer una credencial lo frenan
12
+ ahora los guards, y lo que la persona pide en el chat pasa (caso 104). Si ya
10
13
  existe una versión distinta de un archivo, se detiene sin sobrescribirla para no destruir
11
14
  personalizaciones del proyecto. También crea `CLAUDE.md` cuando no existe y conserva uno existente.
12
15
 
@@ -4,7 +4,32 @@
4
4
  "command": "claude",
5
5
  "config": {
6
6
  "source": "settings.json",
7
- "target": ".claude/settings.json"
7
+ "target": ".claude/settings.json",
8
+ "retired": {
9
+ "permissions": {
10
+ "deny": [
11
+ "Read(.env)",
12
+ "Read(.env.local)",
13
+ "Read(.env.*.local)",
14
+ "Read(.npmrc)",
15
+ "Read(.netrc)",
16
+ "Read(_netrc)",
17
+ "Read(.pypirc)",
18
+ "Read(.dockercfg)",
19
+ "Read(id_rsa)",
20
+ "Read(id_dsa)",
21
+ "Read(id_ecdsa)",
22
+ "Read(id_ed25519)",
23
+ "Read(credentials)",
24
+ "Read(credentials*.json)",
25
+ "Read(*service-account*.json)",
26
+ "Read(*.pem)",
27
+ "Read(*.key)",
28
+ "Read(accesos.md)",
29
+ "Read(credenciales*)"
30
+ ]
31
+ }
32
+ }
8
33
  },
9
34
  "instructions": [
10
35
  {
@@ -12,6 +12,19 @@
12
12
  "hooks": [
13
13
  { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh" }
14
14
  ]
15
+ },
16
+ {
17
+ "matcher": "Read|Grep",
18
+ "hooks": [
19
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh" }
20
+ ]
21
+ }
22
+ ],
23
+ "UserPromptSubmit": [
24
+ {
25
+ "hooks": [
26
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-chat.sh" }
27
+ ]
15
28
  }
16
29
  ],
17
30
  "Stop": [
@@ -18,6 +18,10 @@ porque cambia el hash. No se usa `--dangerously-bypass-hook-trust`.
18
18
  Los `matcher` filtran el **nombre de la herramienta**: los comandos de shell llegan como `Bash` y las
19
19
  ediciones como `apply_patch`, `Edit` o `Write`. No son los nombres internos del protocolo.
20
20
 
21
+ `UserPromptSubmit` registra el mensaje de la persona, para que lo que pidió en el chat pase sin aprobarlo
22
+ a mano. Codex le pasa a cada herramienta el `turn_id` del mensaje que la originó, que es lo que ata la
23
+ llamada al pedido.
24
+
21
25
  Si actualizás una instalación anterior a este cambio, `.codex/hooks/hooks.json` queda huérfano —Codex
22
26
  nunca lo leyó— y se borra a mano.
23
27
 
@@ -14,6 +14,13 @@
14
14
  ]
15
15
  }
16
16
  ],
17
+ "UserPromptSubmit": [
18
+ {
19
+ "hooks": [
20
+ { "type": "command", "command": "{{OPS_ROOT}}/automatization/hooks/guard-chat.sh" }
21
+ ]
22
+ }
23
+ ],
17
24
  "SessionEnd": [
18
25
  {
19
26
  "hooks": [
@@ -17,8 +17,12 @@ Antes vivían bajo `/ops:` y eran tres: el arranque y el recorrido de equipo le
17
17
  así que alguien que venía de otro runner los buscaba en la lista y no estaban. Si actualizás una
18
18
  instalación vieja, `.gemini/commands/ops/` queda huérfano y se borra a mano.
19
19
 
20
- Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool` y `AfterAgent` en
20
+ Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool`, `BeforeAgent` y `AfterAgent` en
21
21
  `.gemini/settings.json`, declarados en `manifest.json`. Sólo corren si la carpeta está marcada como
22
- confiable —`GEMINI.md` explica qué avisa Gemini cuando no lo está—.
22
+ confiable —`GEMINI.md` explica qué avisa Gemini cuando no lo está—. `read_file` pasa por
23
+ `guard-secrets-read.sh`, que frena leer una credencial; un `cat` por `run_shell_command` lo frena
24
+ `secrets-shell`, en el grupo de shell.
25
+ `BeforeAgent` registra el mensaje de la persona, para que lo que pidió en el chat pase sin aprobarlo a
26
+ mano; Gemini no le pasa a cada herramienta de qué mensaje viene, así que ahí vale el último.
23
27
 
24
28
  Comprueba la instalación con `node tools/ops.js automation doctor . gemini`.
@@ -27,6 +27,25 @@
27
27
  "command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh"
28
28
  }
29
29
  ]
30
+ },
31
+ {
32
+ "matcher": "read_file|grep_search",
33
+ "hooks": [
34
+ {
35
+ "type": "command",
36
+ "command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh"
37
+ }
38
+ ]
39
+ }
40
+ ],
41
+ "BeforeAgent": [
42
+ {
43
+ "hooks": [
44
+ {
45
+ "type": "command",
46
+ "command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-chat.sh"
47
+ }
48
+ ]
30
49
  }
31
50
  ],
32
51
  "AfterAgent": [
@@ -322,7 +322,7 @@ function uninstall(root, name, output = console) {
322
322
 
323
323
  if (fs.existsSync(paths.configTarget)) {
324
324
  const current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
325
- const clean = unmergeConfig(current, runnerConfig(paths, root))
325
+ const clean = unmergeConfig(unmergeConfig(current, runnerConfig(paths, root)), runner.config.retired || {})
326
326
  if (clean && Object.keys(clean).length) F.atomicWriteJson(paths.configTarget, clean)
327
327
  else { removeFile(paths.configTarget, paths.install); removed += 1 }
328
328
  output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
@@ -389,6 +389,14 @@ function install(root, name, output = console, options = {}) {
389
389
  ? { config: {}, dropped: [] }
390
390
  : withoutDeliveredHooks(current, live, previous)
391
391
  reportRemoved(name, clean.dropped, live, output)
392
+ // Lo que Cauce entregó en una versión y retiró en otra sin ser un hook, que el merge no saca: las reglas
393
+ // `permissions.deny` que puso el 092 y sacó el 104. Se va exactamente lo que el adaptador declara como
394
+ // retirado; una regla de la empresa que no coincide letra por letra se queda.
395
+ if (runner.config.retired) {
396
+ const before = JSON.stringify(clean.config)
397
+ clean.config = unmergeConfig(clean.config, runner.config.retired) || {}
398
+ if (JSON.stringify(clean.config) !== before) output.log(`− ${name}: quitadas las reglas que Cauce ya no entrega`)
399
+ }
392
400
  F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
393
401
  // Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
394
402
  // el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
@@ -32,6 +32,7 @@ const FLAGS = {
32
32
  adopt: [],
33
33
  agents: ['--json', '--own', '--system'],
34
34
  integration: ['--fixture'],
35
+ secrets: [],
35
36
  automation: ['--force'],
36
37
  learn: ['--flow', '--proposal', '--applied', '--archived', '--period'],
37
38
  evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
@@ -97,4 +97,4 @@ async function run(root, opciones, deps) {
97
97
  return { runner, provider, installed: true }
98
98
  }
99
99
 
100
- module.exports = { run, NO_RUNNER, NO_PROVIDER }
100
+ module.exports = { run, validate, NO_RUNNER, NO_PROVIDER }
package/engine/cli/ops.js CHANGED
@@ -95,11 +95,6 @@ async function init(target, cli) {
95
95
  if (existing.length && !force) {
96
96
  fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
97
97
  }
98
- IN.scaffold(root, { name, mode, force })
99
- const relative = path.relative(process.cwd(), root)
100
- const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
101
- console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
102
-
103
98
  // Preguntar exige una terminal, e instalar baja un paquete y escribe `node_modules`: las dos cosas
104
99
  // pasan cuando hay alguien mirando. Una corrida automatizada —CI, un contenedor, estas pruebas—
105
100
  // recibe la instancia materializada y decide por bandera, sin descargas ni preguntas implícitas.
@@ -112,6 +107,13 @@ async function init(target, cli) {
112
107
  interactive,
113
108
  install: cli.has('--install') || (interactive && !cli.has('--no-install')),
114
109
  }
110
+ // Se valida antes de escribir: un runner o una integración que no existen no pueden dejar una
111
+ // instancia hecha con el comando en error (caso 096). `BOOT.run` vuelve a validar con la misma función.
112
+ try { BOOT.validate(options) } catch (error) { fail(error.message, 2) }
113
+ IN.scaffold(root, { name, mode, force })
114
+ const relative = path.relative(process.cwd(), root)
115
+ const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
116
+ console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
115
117
  let result
116
118
  try {
117
119
  result = await BOOT.run(root, options, {
@@ -166,6 +168,7 @@ function usage() {
166
168
  ops integration rebase <ops-root> <provider> <remote-key>
167
169
  ops integration reconcile <ops-root> <provider> <remote-key>
168
170
  ops integration writeback-plan <ops-root> <provider>
171
+ ops secrets check <ops-root>
169
172
  ops automation list <ops-root>
170
173
  ops automation list-hooks <ops-root>
171
174
  ops automation check <ops-root>
@@ -218,6 +221,7 @@ async function run(cli) {
218
221
  else if (command === 'integration') {
219
222
  await W.integration(arg[1], arg[2], arg[3], arg[4], cli)
220
223
  }
224
+ else if (command === 'secrets') W.secrets(arg[1], arg[2])
221
225
  else if (command === 'automation') W.automation(arg[1], arg[2], arg[3], cli)
222
226
  else if (command === 'learn') CAT.learn(arg[1], cli)
223
227
  else if (command === 'evaluate') CAT.evaluate(arg[1], arg[2], cli)
@@ -8,6 +8,7 @@ const fs = require('node:fs')
8
8
  const path = require('node:path')
9
9
  const F = require('../core/files')
10
10
  const I = require('../integrations/registry')
11
+ const SE = require('../secrets')
11
12
  const A = require('../automation')
12
13
  const SC = require('../core/scan')
13
14
  const OB = require('../core/onboarding')
@@ -38,11 +39,18 @@ const INTEGRATION = {
38
39
  missing: 'Falta <provider>.',
39
40
  run: (root, provider) => {
40
41
  const source = path.join(IN.PROJECT_ROOT, 'template', 'integrations', provider)
41
- if (!fs.existsSync(source)) fail(`Cauce no trae un adaptador para ${provider}.`, 2)
42
+ const fromCauce = fs.existsSync(source)
43
+ // Sin molde en Cauce no hay andamiaje que reponer: habilitar un proveedor propio es sólo el interruptor.
44
+ const own = !fromCauce && (providerRegistry(root).config.providers || {})[provider]
45
+ && fs.existsSync(path.join(root, 'integrations', provider))
46
+ if (!fromCauce && !own) {
47
+ fail(`Cauce no trae un adaptador para ${provider}. Uno propio vive en integrations/${provider}/ y se `
48
+ + `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`, 2)
49
+ }
42
50
  // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
43
51
  // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
44
52
  providerRegistry(root)
45
- IN.copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
53
+ if (fromCauce) IN.copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
46
54
  switchProvider(root, provider, true)
47
55
  console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
48
56
  // Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
@@ -277,8 +285,18 @@ function automation(action, rootArg, runnerName, cli) {
277
285
  fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
278
286
  }
279
287
 
288
+ function secrets(action, rootArg) {
289
+ if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`, 2)
290
+ const result = SE.check(opsRoot(rootArg))
291
+ if (!result.declared) return console.log(`Sin ${SE.DECLARATION}: no hay contrato de secretos que comprobar.`)
292
+ for (const warning of result.warnings) console.log(`⚠ ${warning}`)
293
+ for (const error of result.errors) console.error(`✗ ${error}`)
294
+ if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos`)
295
+ console.log(`✓ contrato de secretos: ${result.current} servicio(s) al día`)
296
+ }
297
+
280
298
  // `init` enciende un proveedor en la misma corrida en que crea la instancia, y ésta es la operación
281
299
  // que lo hace: se expone para que la composición no tenga que conocer la tabla entera.
282
300
  const enableProvider = (root, provider) => INTEGRATION.enable.run(root, provider)
283
301
 
284
- module.exports = { scan, onboard, integration, automation, enableProvider }
302
+ module.exports = { scan, onboard, integration, automation, secrets, enableProvider }
@@ -10,7 +10,9 @@ const path = require('node:path')
10
10
 
11
11
  // `~` se expande sólo cuando es el prefijo entero. `~datos` es un nombre de directorio válido y no la
12
12
  // casa de nadie; expandirlo ahí convertiría una ruta relativa en una absoluta que el autor no escribió.
13
- function resolve(root, entry) {
13
+ // La usa también el contrato de secretos para las rutas de sus identidades, por la misma razón que
14
+ // arriba: una ruta declarada en un archivo del proyecto se resuelve igual la lea quien la lea.
15
+ function resolvePath(root, entry) {
14
16
  return path.resolve(root, String(entry).replace(/^~(?=$|[/\\])/, os.homedir()))
15
17
  }
16
18
 
@@ -21,7 +23,7 @@ function resolve(root, entry) {
21
23
  function writableOutsideRoots(root, config) {
22
24
  const declared = config && Array.isArray(config.writableOutsideRoots) ? config.writableOutsideRoots : []
23
25
  return declared.filter((entry) => typeof entry === 'string' && entry.trim())
24
- .map((entry) => ({ declared: entry, path: resolve(root, entry) }))
26
+ .map((entry) => ({ declared: entry, path: resolvePath(root, entry) }))
25
27
  }
26
28
 
27
- module.exports = { writableOutsideRoots }
29
+ module.exports = { writableOutsideRoots, resolvePath }
@@ -20,9 +20,14 @@
20
20
  //
21
21
  // Queda a la vista porque `check` avisa mientras exista. Sin eso, un archivo olvidado sigue autorizando
22
22
  // esas mismas rutas la próxima vez que alguien las stagee, que es la puerta abierta que esto evitaba.
23
+ //
24
+ // El archivo es la vía de cuando no hay chat. Con una persona hablando, lo que ella pidió ya está
25
+ // aprobado —cómo se sabe, en `chat.js`—, y escribir el archivo deja de ser necesario (caso 098).
23
26
 
24
27
  const path = require('node:path')
25
28
  const fs = require('node:fs')
29
+ const { opsRoot, cwdOf } = require('./input')
30
+ const CHAT = require('./chat')
26
31
 
27
32
  const APPROVAL = '.ops-approval'
28
33
 
@@ -35,16 +40,45 @@ function read(root) {
35
40
  }
36
41
 
37
42
  // Qué queda sin aprobar de lo que un guard está por bloquear. Se reporta sólo eso: mandar a revisar lo
38
- // que ya se aprobó es lo que hace que la próxima vez nadie lea el mensaje.
39
- function pending(root, files) {
43
+ // que ya se aprobó es lo que hace que la próxima vez nadie lea el mensaje. Cuenta también lo que la
44
+ // persona pidió en el chat.
45
+ function pending(root, files, input) {
40
46
  const approved = new Set(root ? read(root) : [])
41
- return files.filter((file) => !approved.has(file))
47
+ return CHAT.unauthorized(input, files.filter((file) => !approved.has(file)))
48
+ }
49
+
50
+ // El archivo que el guard va a leer, nombrado desde la carpeta en la que está la sesión. En sidecar la
51
+ // sesión se abre en el workspace y la instancia es una subcarpeta, así que `planning/` a secas nombraba
52
+ // otro directorio y pegar ahí no destrababa nada (caso 097).
53
+ function where(input) {
54
+ const root = opsRoot(input)
55
+ if (!root) return `planning/${APPROVAL}`
56
+ const file = path.join(root, 'planning', APPROVAL)
57
+ const session = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || cwdOf(input)
58
+ const relative = path.relative(session, file)
59
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file
42
60
  }
43
61
 
44
- // Cómo se toma la salida angosta, dicho una vez porque ahora lo dicen cinco bloqueos. Nombra también la
45
- // variable: sigue existiendo, y esconderla haría que quien la necesite la descubra sin saber su alcance.
46
- const HOW = (variable) => `Aprobalo escribiendo esa(s) ruta(s) en planning/${APPROVAL}, una por línea: `
47
- + `vale para ese conjunto y deja de valer en cuanto cambie. La variable ${variable}=1 sigue existiendo `
48
- + 'y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.'
62
+ // Cómo se toma la salida angosta, dicho una vez porque lo dicen todos los bloqueos que la tienen. Lleva
63
+ // las líneas exactas porque cada guard coteja la ruta en la forma que tiene a mano —absoluta la que llega
64
+ // de un Write, relativa al repositorio la que sale del índice— y una línea en la otra forma no pega: sin
65
+ // decirla, lo que quedaba a mano era la variable (caso 089). Nombra también la variable: sigue
66
+ // existiendo, y esconderla haría que quien la necesite la descubra sin saber su alcance.
67
+ //
68
+ // Con una persona en el chat, además, deja anotado lo que se frenó —para eso llama a `hold`— y lo dice
69
+ // primero: contestar es más corto que editar un archivo, y es lo que la persona ya está haciendo. El
70
+ // archivo queda como cosa de ella: dicho en imperativo, el agente leía «aprobalo» como una orden para él
71
+ // e intentaba escribírselo en vez de reintentar, medido en una sesión real de Claude Code.
72
+ function HOW(variable, lines, input) {
73
+ const chat = CHAT.hold(input, lines)
74
+ return (chat
75
+ ? 'Decile a la persona qué se frenó y por qué, y esperá: si contesta «dale», reintentá el mismo cambio y '
76
+ + 'pasa. Si prefiere aprobarlo a mano, que pegue ella tal cual en'
77
+ : 'Aprobalo pegando tal cual en')
78
+ + ` ${where(input)} estas líneas:\n`
79
+ + lines.map((line) => ` ${line}\n`).join('')
80
+ + `Valen para ese conjunto y dejan de valer en cuanto cambie. La variable ${variable}=1 sigue existiendo `
81
+ + 'y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.'
82
+ }
49
83
 
50
84
  module.exports = { APPROVAL, read, pending, HOW }