@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 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 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,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
@@ -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
@@ -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. Si ya
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
- write(`Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
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.`, { label: 'plan-human' })
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
- await write(`Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
575
- { label: 'ready-human' })
576
- return stop('not-ready', ready.reason)
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
- await write(`Registrá ${task.id} en ${HUMAN}: el criterio "${ambiguous.criterion}" no dice qué habría ` +
797
- `que aserciar, y hace falta la decisión que lo fija.`, { label: 'verify-human' })
798
- return stop('acceptance-ambiguous', ambiguous.criterion)
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 ` +
@@ -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 }
@@ -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 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 `
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 }