@ingeniomaps/cauce 0.78.0 → 0.80.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 +119 -0
- package/README.md +6 -3
- package/automatization/hooks/README.md +18 -2
- package/automatization/hooks/guard-secrets-read.sh +3 -0
- package/automatization/runners/claude/README.md +3 -1
- package/automatization/runners/claude/settings.json +29 -0
- package/automatization/runners/gemini/README.md +2 -1
- package/automatization/runners/gemini/settings.json +9 -0
- package/automatization/workflows/autobuild.js +21 -10
- package/engine/cli/args.js +1 -0
- package/engine/cli/bootstrap.js +1 -1
- package/engine/cli/ops.js +9 -5
- package/engine/cli/wiring.js +21 -3
- package/engine/config/paths.js +5 -3
- package/engine/hooks/approval.js +8 -4
- package/engine/hooks/files.js +104 -27
- package/engine/hooks/run.js +7 -0
- package/engine/hooks/shell.js +34 -17
- package/engine/integrations/providers/jira.js +1 -1
- package/engine/integrations/registry.js +28 -5
- package/engine/secrets/index.js +201 -0
- package/package.json +1 -1
- package/template/AGENTS.md +1 -0
- package/template/integrations/README.md +21 -0
- package/template/organization/README.md +52 -0
- package/template/planning/adr/README.md +1 -0
- package/template/planning/adr/system/OPS-007-contrato-de-secretos-compartido.md +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,125 @@ 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.80.0] - 2026-09-10
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **Leer una credencial también se frena, y las identidades declaradas cuentan como credencial.** Un guard
|
|
22
|
+
nuevo, `secrets-read`, corre en la herramienta de lectura de Claude (`Read`) y de Gemini (`read_file`) y
|
|
23
|
+
frena los mismos archivos que `secrets` frena al escribir. Esos archivos ahora incluyen las identidades
|
|
24
|
+
`source: file` de `organization/secrets.json`, que antes pasaban por no tener nombre de credencial. En
|
|
25
|
+
Claude, además, la instalación agrega reglas `permissions.deny` `Read(...)` que el propio Claude aplica
|
|
26
|
+
también a `cat`, `head`, `tail`, `sed` y redirecciones.
|
|
27
|
+
|
|
28
|
+
**Qué cambia para vos**: el agente deja de poder leer `.env`, claves y tokens conocidos; `.env.example`
|
|
29
|
+
sigue legible. Si una lectura hace falta, aprobá la ruta en `planning/.ops-approval`. Corré
|
|
30
|
+
`automation install` para que tu `.claude/settings.json` reciba las reglas: se suman a las tuyas. No es un
|
|
31
|
+
límite de seguridad —un `grep -r` o un script propio siguen leyendo—, y en Codex y Antigravity no hay guard
|
|
32
|
+
de lectura.
|
|
33
|
+
|
|
34
|
+
- **Un proveedor de integraciones propio, sin tocar Cauce.** El registro de `integrations/config.json`
|
|
35
|
+
ya tenía un campo `adapter` que nadie leía: ahora `"adapter": "./adapter.js"` carga el adaptador de la
|
|
36
|
+
empresa desde `integrations/<nombre>/`, e `integration enable` lo conecta sin exigir un molde de Cauce.
|
|
37
|
+
El adaptador declara `contract: 1` y las tres funciones del contrato, y `check` rechaza el que no
|
|
38
|
+
cumpla.
|
|
39
|
+
|
|
40
|
+
**Qué cambia para vos**: nada si sólo usás Jira. Para otra herramienta, `integrations/README.md` de tu
|
|
41
|
+
instancia trae el recorrido.
|
|
42
|
+
|
|
43
|
+
- **Un contrato de secretos compartido entre repositorios, y un chequeo sin red.** Si tus servicios
|
|
44
|
+
usan un gestor de secretos, los scripts y workflows que lo conectan terminan copiados en cada
|
|
45
|
+
repositorio, y el arreglo que alguien hizo en uno no llega a los demás. `organization/secrets.json`
|
|
46
|
+
declara qué cuenta, qué identidad y qué archivos comparte cada servicio, y
|
|
47
|
+
`node tools/ops.js secrets check .` compara cada copia contra la canónica que guarda la instancia:
|
|
48
|
+
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
|
|
49
|
+
repositorio o si la declaración guarda un valor en vez de una referencia.
|
|
50
|
+
|
|
51
|
+
**Qué cambia para vos**: nada si no lo usás. Para adoptarlo, `organization/README.md` trae el formato
|
|
52
|
+
y el recorrido. Cauce no se conecta a ningún gestor ni trae adaptadores: lo que habla con el gestor
|
|
53
|
+
sigue siendo tuyo. Si tus proyectos tienen instancias separadas, el chequeo sólo compara dentro de
|
|
54
|
+
cada una: para que un esqueleto y sus derivados se midan contra lo mismo, van como raíces de la misma
|
|
55
|
+
instancia.
|
|
56
|
+
|
|
57
|
+
### Corregido
|
|
58
|
+
|
|
59
|
+
- **`init` rechaza un runner mal escrito sin crear la instancia.** Con `--runner none` —el valor es
|
|
60
|
+
`ninguno`— o una `--integration` que no existe, `init` escribía la instancia entera y recién después
|
|
61
|
+
salía con error: el código de salida decía que no había pasado nada y el segundo intento encontraba la
|
|
62
|
+
carpeta creada. Ahora valida los dos valores antes de escribir.
|
|
63
|
+
|
|
64
|
+
**Qué cambia para vos**: nada con un valor correcto. Con uno mal escrito, el destino queda como estaba.
|
|
65
|
+
|
|
66
|
+
- **El bloqueo de `verify` cita la prueba que falló.** Mostraba la primera línea de la salida que dijera
|
|
67
|
+
«error», y en un reporte de pruebas ésa puede ser una verde cuyo nombre lo dice. Ahora, con la salida de
|
|
68
|
+
`node --test` —spec y TAP— y de `go test`, cita la primera prueba en rojo; con otras herramientas sigue
|
|
69
|
+
buscando por palabra, pero ya no elige una línea marcada como verde.
|
|
70
|
+
|
|
71
|
+
**Qué cambia para vos**: el mensaje apunta a la prueba que hay que mirar.
|
|
72
|
+
|
|
73
|
+
- **Cada bloqueo con salida angosta dice qué líneas pegar.** Aprobar en `planning/.ops-approval` sólo
|
|
74
|
+
funciona si la ruta está escrita en la forma que ese guard coteja —absoluta la de un `Write`, relativa
|
|
75
|
+
al repositorio la de un commit—, y el mensaje decía «escribí esa(s) ruta(s)» sin nombrarlas. `verify`
|
|
76
|
+
y `dependencies` ni siquiera mostraban la línea. Ahora cada bloqueo imprime las líneas exactas, y
|
|
77
|
+
pegarlas tal cual destraba ese mismo bloqueo.
|
|
78
|
+
|
|
79
|
+
**Qué cambia para vos**: si una aprobación no pegaba y terminabas exportando la variable del guard,
|
|
80
|
+
pegá lo que dice el mensaje.
|
|
81
|
+
|
|
82
|
+
- **`plan-first` deja de frenar la configuración y los archivos de la instancia.** Frenaba
|
|
83
|
+
`ops.config.json` —justo el archivo que el límite de raíces manda a editar para declarar una ruta— y,
|
|
84
|
+
en sidecar, también `AGENTS.md`, `CLAUDE.md`, `package.json` y `.gitignore` de la instancia, como si
|
|
85
|
+
fueran producto. Ahora producto es el código de una raíz declarada: la instancia sidecar no lo es
|
|
86
|
+
aunque viva dentro de su raíz (`..`), y lo que queda fuera de toda raíz —lo declarado en
|
|
87
|
+
`writableOutsideRoots`— tampoco. En embedded, el `package.json` de la raíz sigue siendo producto.
|
|
88
|
+
|
|
89
|
+
**Qué cambia para vos**: si aprobabas esas rutas a mano en `.ops-approval` o exportabas
|
|
90
|
+
`OPS_PLAN_FIRST_OVERRIDE` para poder tocarlas, ya no hace falta.
|
|
91
|
+
|
|
92
|
+
- **`verify` deja afuera del índice de su copia lo que enlaza.** Cuando el árbol difiere del índice, el
|
|
93
|
+
guard corre los gates sobre una copia del índice con lo ignorado enlazado —`node_modules`, `.env`—. Si
|
|
94
|
+
tu `.gitignore` escribe ese directorio con barra final (`node_modules/`), el enlace entraba al índice de
|
|
95
|
+
la copia, y un gate que recorre lo trackeado —`git ls-files`— lo recibía como si fuera parte del commit.
|
|
96
|
+
|
|
97
|
+
**Qué cambia para vos**: si una suite que pasa a mano fallaba bajo `verify` sólo cuando tenías archivos
|
|
98
|
+
sin trackear, podía ser esto.
|
|
99
|
+
|
|
100
|
+
## [0.79.0] - 2026-09-10
|
|
101
|
+
|
|
102
|
+
### Corregido
|
|
103
|
+
|
|
104
|
+
- **La fila que registra una parada se escribe antes de parar.** Cuando ningún plan sobrevive a la
|
|
105
|
+
crítica, el recorrido pide que la tarea quede en `HUMAN_ACTIONS.md` con su motivo —así `context` deja
|
|
106
|
+
de ofrecerla y alguien la ve—. Esa escritura se lanzaba y el recorrido volvía en la línea siguiente,
|
|
107
|
+
así que el agente que la escribía se quedaba a mitad de camino: en una corrida real el resumen contó
|
|
108
|
+
diez agentes y el registro nueve. La parada se informaba bien y el disco no la tenía.
|
|
109
|
+
|
|
110
|
+
Ahora se espera. Y en las tres paradas que dejan fila —plan rechazado, tarea que no está lista,
|
|
111
|
+
criterio que no dice qué aserciar— si el agente no contesta, el detalle de la parada lo dice: el
|
|
112
|
+
motivo sigue siendo el que la causó, y se agrega que la fila hay que escribirla a mano.
|
|
113
|
+
|
|
114
|
+
**Qué cambia para vos**: relanzar después de un plan rechazado deja de repetir la planificación
|
|
115
|
+
entera, porque la tarea queda registrada. Si venías viendo que `context` te ofrecía otra vez la tarea
|
|
116
|
+
que acababa de rechazarse, era esto.
|
|
117
|
+
|
|
118
|
+
- **El guard de migraciones frena por haber viajado, no por estar en disco.** Escribir una migración son
|
|
119
|
+
dos pasos —crearla y completarla— y el segundo se bloqueaba: el chequeo preguntaba si el archivo
|
|
120
|
+
existía, que no es la pregunta. Un stub de hace dos segundos y una migración publicada hace un año
|
|
121
|
+
daban la misma respuesta, y **ninguna herramienta lo esquivaba** —`Write`, `Edit` y `MultiEdit` se
|
|
122
|
+
bloqueaban igual—, así que lo único que quedaba a la vista era apagar el guard entero, incluida la
|
|
123
|
+
protección contra SQL destructivo.
|
|
124
|
+
|
|
125
|
+
Ahora se le pregunta a git si el archivo está en `HEAD`. En el índice no alcanza: un archivo apenas
|
|
126
|
+
agregado no viajó a ninguna parte. Y sin repositorio con el que contestar —un proyecto sin git— se
|
|
127
|
+
conserva la conducta anterior en vez de dejar pasar, que es el lado correcto para equivocarse cuando
|
|
128
|
+
no se puede saber.
|
|
129
|
+
|
|
130
|
+
**Qué cambia para vos**: completar una migración recién creada deja de frenarse. El mensaje del
|
|
131
|
+
bloqueo pasa a decir el hecho que lo sostiene —que está en el historial, o que existe y acá no hay con
|
|
132
|
+
qué saberlo— y a nombrar cómo aprobar esa ruta puntual en `planning/.ops-approval`, que es lo que el
|
|
133
|
+
bloqueo hermano ya hacía. Si tenías `OPS_MIGRATIONS_OVERRIDE=1` puesto para poder trabajar, ya no hace
|
|
134
|
+
falta y conviene sacarlo: apagaba también lo que sí te cuida.
|
|
135
|
+
|
|
17
136
|
## [0.78.0] - 2026-09-10
|
|
18
137
|
|
|
19
138
|
### 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
|
|
318
|
-
`
|
|
319
|
-
promoción y validación no se reimplementan.
|
|
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,20 @@ 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
|
+
`secrets-read` frena leer, con la herramienta de lectura del runner, un archivo que `secrets` frenaría al
|
|
51
|
+
escribir —los nombres conocidos y las identidades que declara `organization/secrets.json`—. Qué alcanza en
|
|
52
|
+
cada runner es distinto, y conviene saberlo:
|
|
53
|
+
|
|
54
|
+
- **Claude Code**: el guard corre en `Read`, y además la instalación agrega reglas `permissions.deny`
|
|
55
|
+
`Read(...)` por los nombres conocidos. Esas reglas las aplica el propio Claude Code a `Read`, a `cat`,
|
|
56
|
+
`head`, `tail`, `sed` y a las redirecciones, pero no a un `grep -r` ni a un subproceso que abra el archivo
|
|
57
|
+
por su cuenta. Van por nombre exacto: `.env.*` también negaría `.env.example`.
|
|
58
|
+
- **Gemini CLI**: el guard corre en `read_file`. Un `cat` por `run_shell_command` no lo ve.
|
|
59
|
+
- **Codex y Antigravity**: sin guard de lectura. Sus adaptadores sólo enganchan shell y edición, y leer por
|
|
60
|
+
shell no pasa por ningún matcher de archivo.
|
|
61
|
+
|
|
47
62
|
## Cómo se ejecutan
|
|
48
63
|
|
|
49
64
|
```text
|
|
@@ -67,8 +82,9 @@ runner, mientras la lógica se prueba y mantiene una sola vez en `engine/hooks/r
|
|
|
67
82
|
|
|
68
83
|
| Grupo | Guards | Wrapper |
|
|
69
84
|
|---|---|---|
|
|
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` |
|
|
85
|
+
| `pre-shell` | destructive, git-add, dependencies, governance, verify, shell-boundary | `guard-shell.sh` |
|
|
86
|
+
| `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence, plan-first | `guard-files.sh` |
|
|
87
|
+
| `pre-read` | secrets-read | `guard-secrets-read.sh` |
|
|
72
88
|
| `stop` | planning-drift | `guard-planning-drift.sh` |
|
|
73
89
|
|
|
74
90
|
Registrar el grupo gasta un proceso por herramienta en lugar de cinco, con el mismo orden y la misma
|
|
@@ -6,7 +6,9 @@ Adaptador nativo mediante `PreToolUse` y `Stop`. Instalar con:
|
|
|
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.
|
|
9
|
+
El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves. También
|
|
10
|
+
suma a `permissions.deny` reglas `Read(...)` por los nombres de credencial conocidos, junto a las que el
|
|
11
|
+
proyecto ya tenga; una que Cauce retire en una versión futura no se quita sola. Si ya
|
|
10
12
|
existe una versión distinta de un archivo, se detiene sin sobrescribirla para no destruir
|
|
11
13
|
personalizaciones del proyecto. También crea `CLAUDE.md` cuando no existe y conserva uno existente.
|
|
12
14
|
|
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(.env)",
|
|
5
|
+
"Read(.env.local)",
|
|
6
|
+
"Read(.env.*.local)",
|
|
7
|
+
"Read(.npmrc)",
|
|
8
|
+
"Read(.netrc)",
|
|
9
|
+
"Read(_netrc)",
|
|
10
|
+
"Read(.pypirc)",
|
|
11
|
+
"Read(.dockercfg)",
|
|
12
|
+
"Read(id_rsa)",
|
|
13
|
+
"Read(id_dsa)",
|
|
14
|
+
"Read(id_ecdsa)",
|
|
15
|
+
"Read(id_ed25519)",
|
|
16
|
+
"Read(credentials)",
|
|
17
|
+
"Read(credentials*.json)",
|
|
18
|
+
"Read(*service-account*.json)",
|
|
19
|
+
"Read(*.pem)",
|
|
20
|
+
"Read(*.key)",
|
|
21
|
+
"Read(accesos.md)",
|
|
22
|
+
"Read(credenciales*)"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
2
25
|
"hooks": {
|
|
3
26
|
"PreToolUse": [
|
|
4
27
|
{
|
|
@@ -12,6 +35,12 @@
|
|
|
12
35
|
"hooks": [
|
|
13
36
|
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh" }
|
|
14
37
|
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"matcher": "Read",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh" }
|
|
43
|
+
]
|
|
15
44
|
}
|
|
16
45
|
],
|
|
17
46
|
"Stop": [
|
|
@@ -19,6 +19,7 @@ instalación vieja, `.gemini/commands/ops/` queda huérfano y se borra a mano.
|
|
|
19
19
|
|
|
20
20
|
Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool` 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` no lo ve.
|
|
23
24
|
|
|
24
25
|
Comprueba la instalación con `node tools/ops.js automation doctor . gemini`.
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
"command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "read_file",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
30
39
|
}
|
|
31
40
|
],
|
|
32
41
|
"AfterAgent": [
|
|
@@ -355,6 +355,14 @@ const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
|
|
|
355
355
|
const run = (prompt, options = {}) => agent(`${SCOPE}\n\n${prompt}`, options)
|
|
356
356
|
const write = (prompt, options = {}) => agent(`${LEDGER}\n\n${prompt}`, options)
|
|
357
357
|
|
|
358
|
+
// Las tres paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es
|
|
359
|
+
// el único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso
|
|
360
|
+
// se espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó.
|
|
361
|
+
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó pedida. Caso 087.
|
|
362
|
+
const registerHuman = async (prompt, label) => (await write(prompt, { label })
|
|
363
|
+
? ''
|
|
364
|
+
: ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`)
|
|
365
|
+
|
|
358
366
|
// Gate, mutex de WIP y selección de tarea salen de un comando determinista: AWAITING_REVIEW, BACKLOG,
|
|
359
367
|
// WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
|
|
360
368
|
const readContext = () => read(
|
|
@@ -552,12 +560,13 @@ while (rounds++ < MAX_TASKS) {
|
|
|
552
560
|
//
|
|
553
561
|
// Lo que la fila le pide a una persona lo dice R17: dos rechazos sobre lo mismo son el disparador
|
|
554
562
|
// posterior de división. No se parte acá porque partir es una decisión, y ésa no le toca al recorrido.
|
|
555
|
-
const planRejected = (reason, unit, found) => {
|
|
563
|
+
const planRejected = async (reason, unit, found) => {
|
|
556
564
|
const detail = found.join('; ') || 'sin condiciones nombradas'
|
|
557
|
-
|
|
565
|
+
const nota = await registerHuman(
|
|
566
|
+
`Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
|
|
558
567
|
+ `Motivo: ${detail}. La acción humana es revisar si la unidad son dos resultados con vidas `
|
|
559
|
-
+ `distintas y partirla —R17—, o dejarla entera con la razón escrita.`,
|
|
560
|
-
return stop(reason, detail)
|
|
568
|
+
+ `distintas y partirla —R17—, o dejarla entera con la razón escrita.`, 'plan-human')
|
|
569
|
+
return stop(reason, `${detail}${nota}`)
|
|
561
570
|
}
|
|
562
571
|
|
|
563
572
|
if (!planning.wipActive) {
|
|
@@ -571,9 +580,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
571
580
|
)
|
|
572
581
|
if (!ready) return stop('agent-unavailable', 'Ready no devolvió resultado')
|
|
573
582
|
if (!ready.ready) {
|
|
574
|
-
|
|
575
|
-
{
|
|
576
|
-
|
|
583
|
+
const nota = await registerHuman(
|
|
584
|
+
`Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
|
|
585
|
+
'ready-human')
|
|
586
|
+
return stop('not-ready', `${ready.reason}${nota}`)
|
|
577
587
|
}
|
|
578
588
|
if (ready.refinedAcceptance) task.acceptance = ready.refinedAcceptance
|
|
579
589
|
}
|
|
@@ -793,9 +803,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
793
803
|
// hacer parar a una persona por eso le cobra una interrupción por algo que se resolvía solo.
|
|
794
804
|
const ambiguous = verified.uncovered.find((entry) => entry.cause === 'ambiguous')
|
|
795
805
|
if (ambiguous) {
|
|
796
|
-
|
|
797
|
-
`
|
|
798
|
-
|
|
806
|
+
const nota = await registerHuman(
|
|
807
|
+
`Registrá ${task.id} en ${HUMAN}: el criterio "${ambiguous.criterion}" no dice qué habría ` +
|
|
808
|
+
`que aserciar, y hace falta la decisión que lo fija.`, 'verify-human')
|
|
809
|
+
return stop('acceptance-ambiguous', `${ambiguous.criterion}${nota}`)
|
|
799
810
|
}
|
|
800
811
|
if (verified.uncovered.length) {
|
|
801
812
|
await run(`${asRole(cast.build)}Escribí sólo las pruebas que faltan en ${task.id}, con el mismo rojo ` +
|
package/engine/cli/args.js
CHANGED
|
@@ -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'],
|
package/engine/cli/bootstrap.js
CHANGED
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)
|
package/engine/cli/wiring.js
CHANGED
|
@@ -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
|
-
|
|
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 }
|
package/engine/config/paths.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
26
|
+
.map((entry) => ({ declared: entry, path: resolvePath(root, entry) }))
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
module.exports = { writableOutsideRoots }
|
|
29
|
+
module.exports = { writableOutsideRoots, resolvePath }
|
package/engine/hooks/approval.js
CHANGED
|
@@ -41,10 +41,14 @@ function pending(root, files) {
|
|
|
41
41
|
return files.filter((file) => !approved.has(file))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Cómo se toma la salida angosta, dicho una vez porque
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Cómo se toma la salida angosta, dicho una vez porque lo dicen todos los bloqueos que la tienen. Lleva
|
|
45
|
+
// las líneas exactas porque cada guard coteja la ruta en la forma que tiene a mano —absoluta la que llega
|
|
46
|
+
// de un Write, relativa al repositorio la que sale del índice— y una línea en la otra forma no pega: sin
|
|
47
|
+
// decirla, lo que quedaba a mano era la variable (caso 089). Nombra también la variable: sigue
|
|
48
|
+
// existiendo, y esconderla haría que quien la necesite la descubra sin saber su alcance.
|
|
49
|
+
const HOW = (variable, lines) => `Aprobalo pegando tal cual en planning/${APPROVAL} estas líneas:\n`
|
|
50
|
+
+ lines.map((line) => ` ${line}\n`).join('')
|
|
51
|
+
+ `Valen para ese conjunto y dejan de valer en cuanto cambie. La variable ${variable}=1 sigue existiendo `
|
|
48
52
|
+ 'y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.'
|
|
49
53
|
|
|
50
54
|
module.exports = { APPROVAL, read, pending, HOW }
|