@ingeniomaps/cauce 0.91.0 → 0.93.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 +124 -0
- package/automatization/hooks/run-hook.sh +4 -4
- package/automatization/runners/antigravity/hook.js +2 -2
- package/automatization/workflows/agent-eval.js +1 -1
- package/automatization/workflows/autobuild.js +52 -4
- package/engine/agents/learning.js +0 -2
- package/engine/automation/check.js +95 -0
- package/engine/automation/index.js +7 -74
- package/engine/cli/args.js +1 -0
- package/engine/cli/bench.js +314 -0
- package/engine/cli/catalog.js +7 -168
- package/engine/cli/contract.js +102 -2
- package/engine/cli/ops.js +3 -0
- package/engine/cli/validate.js +2 -0
- package/engine/config/validate.js +13 -1
- package/engine/core/ownership.js +27 -0
- package/engine/core/scope.js +130 -0
- package/engine/hooks/run.js +2 -1
- package/engine/hooks/shell.js +8 -248
- package/engine/hooks/verify.js +275 -0
- package/engine/schemas/ops-config.schema.json +9 -0
- package/package.json +1 -1
- package/template/organization/workspace.md +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,130 @@ 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.93.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Declarar un límite ahora lo saca del aviso, en vez de sumarlo.** Si escribiste tus límites como
|
|
22
|
+
viñetas bajo `### Límites` —el camino que 0.92.0 agregó—, `check` los contaba igual como párrafos que
|
|
23
|
+
no llegan a los agentes, y también contaba la frase con la que presentabas la lista. O sea que hacer
|
|
24
|
+
lo correcto **subía** el número: medido sobre un banco, de 4 párrafos avisados pasaba a 6.
|
|
25
|
+
|
|
26
|
+
Ahora una viñeta declarada y la prosa que la presenta no entran en el aviso. Lo que sigue entrando es
|
|
27
|
+
la prosa de afuera del bloque, que es lo único para lo que el aviso existe: descontar el bloque entero
|
|
28
|
+
la habría silenciado, porque a un bloque `### Límites` no lo cierra nada más que el próximo
|
|
29
|
+
encabezado y el del molde se extiende hasta donde escribas el tuyo.
|
|
30
|
+
|
|
31
|
+
- **El aviso te manda al camino declarado y no a imitar una gramática.** Decía que tus párrafos no
|
|
32
|
+
llegan «porque no arrancan con «El runner», «Debe» o «Nunca»». Desde 0.92.0 hay una forma de
|
|
33
|
+
arreglarlo sin imitar nada, y es la que el aviso nombra ahora: sumar el límite como viñeta bajo
|
|
34
|
+
`### Límites`. Los dos caminos siguen valiendo; lo que cambia es cuál se recomienda.
|
|
35
|
+
|
|
36
|
+
## [0.92.0] - 2026-09-15
|
|
37
|
+
|
|
38
|
+
### Agregado
|
|
39
|
+
|
|
40
|
+
- **El ciclo de aprendizaje ya no deja la propuesta mensual en «por definir».** Cada mes, el ciclo
|
|
41
|
+
consolida lo que recomendaron los informes semanales de un cargo y abre su propuesta. Hasta ahora la
|
|
42
|
+
dejaba con el molde intacto en «Cambio propuesto», y eso no es aprobable: nadie firma una intención.
|
|
43
|
+
Medido sobre `2026-09`: de **25** propuestas, **15** se archivaron con el molde y sólo 10 llegaron a
|
|
44
|
+
algo, las que alguien completó a mano.
|
|
45
|
+
|
|
46
|
+
El recorrido que escribe el cambio exacto —archivo por archivo, contrastado contra los casos
|
|
47
|
+
adversariales vigentes— ya existía; lo que faltaba era que el ciclo lo corriera. Ahora lo corre, y sólo
|
|
48
|
+
sobre las propuestas que quedaron sin decidir.
|
|
49
|
+
|
|
50
|
+
**Sin credencial no se rompe nada**: el paso avisa y el mes queda como quedaba antes, con la rama
|
|
51
|
+
empujada y sus sellos puestos. Lo mismo si la corrida falla. Lo que decide si se te pide una firma
|
|
52
|
+
sigue siendo el documento, no quién lo llenó.
|
|
53
|
+
|
|
54
|
+
- **Tu proyecto puede declarar sus límites en una lista, y `check` avisa del que no llegue a los agentes.**
|
|
55
|
+
Los límites que tu proyecto amplía o restringe viven en `organization/workspace.md`, y de ahí viajan al
|
|
56
|
+
preámbulo de cada subagente. Hasta ahora se reconocían **por cómo arrancaba el párrafo** —«El runner»,
|
|
57
|
+
«Debe», «Nunca»—, que es la gramática del molde: un límite escrito de cualquier otra forma, que es como
|
|
58
|
+
lo escribiría cualquiera, no llegaba a ningún agente y nada lo decía. La lista salía más corta y se leía
|
|
59
|
+
igual de completa.
|
|
60
|
+
|
|
61
|
+
Ahora esa sección trae un `### Límites` con una viñeta por límite:
|
|
62
|
+
|
|
63
|
+
```markdown
|
|
64
|
+
### Límites
|
|
65
|
+
|
|
66
|
+
- En `api/` no se tocan migraciones sin aprobación de quien administra la base.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Lo que ya tenías escrito sigue contando**: la gramática vieja se lee igual, así que no hay nada que
|
|
70
|
+
migrar. Y lo que no entra por ninguno de los dos caminos ya no se pierde callado — `ops check` lo
|
|
71
|
+
reporta citando el párrafo, para que sepas cuál de tus límites se quedó afuera.
|
|
72
|
+
|
|
73
|
+
El ejemplo del molde viene comentado a propósito: un límite de ejemplo que se obedece es peor que
|
|
74
|
+
ninguno, porque nadie lo escribió y todos lo cumplirían.
|
|
75
|
+
|
|
76
|
+
- **Reanudar una tarea dejó de pagar la fase que no tiene nada que hacer, y la corrida dice cuándo lo
|
|
77
|
+
hizo.** Una tarea que para antes de Commit —una revisión que pidió algo, un gate en rojo— deja su plan
|
|
78
|
+
en disco con los pasos tildados. Al relanzar, el recorrido entraba igual a Build: el agente releía el
|
|
79
|
+
WIP, comprobaba el disco y contestaba que no había nada pendiente. Hacía lo correcto; lo que costaba
|
|
80
|
+
era haberlo llamado — **893.000 tokens sobre tres corridas de una sola tarea**, medido.
|
|
81
|
+
|
|
82
|
+
Ahora, si el WIP no tiene pasos pendientes, esa llamada no se hace y la fase se anuncia como
|
|
83
|
+
`Build (reanudado)`, que viaja al resultado de la corrida y a la entrada de DONE. Antes una corrida
|
|
84
|
+
reanudada se veía idéntica a una que construyó salvo por el costo, así que comprobar qué se reutilizó
|
|
85
|
+
exigía abrir la salida cruda y sumar tokens a mano.
|
|
86
|
+
|
|
87
|
+
**Lo que no cambia es qué se revisa.** No se saltea la fase, se saltea la llamada: Review, Verify y QA
|
|
88
|
+
siguen mirando el diff real que quedó en disco, venga de la corrida que venga. Y si tu instancia no
|
|
89
|
+
emite ese dato, todo se comporta como antes.
|
|
90
|
+
|
|
91
|
+
- **Una raíz puede declarar qué rutas lee su puerta, y un archivo sucio que el gate no va a abrir deja de
|
|
92
|
+
forzar la copia del índice.** Al commitear, `verify` corre sobre el árbol cuando árbol e índice
|
|
93
|
+
coinciden y materializa el índice en un temporal cuando difieren. Hasta ahora alcanzaba **cualquier**
|
|
94
|
+
archivo sin trackear para materializar: un README a medio escribir, un `tsconfig.json` que dejó otra
|
|
95
|
+
sesión, o el propio `planning/.ops-approval` que el bloqueo te manda crear para aprobar unas rutas.
|
|
96
|
+
|
|
97
|
+
Ahora, junto a `verify`, una raíz puede declarar `scope`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
"workspaceRoots": [
|
|
101
|
+
{ "name": "web", "path": "apps/web", "verify": "pnpm build",
|
|
102
|
+
"scope": ["src/**", "package.json", "tsconfig.json"] }
|
|
103
|
+
]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Con eso, sólo materializa si alguna ruta que difiere cae dentro de ese alcance. Los patrones son
|
|
107
|
+
relativos a la raíz —el `scope` de `apps/web` habla de `src/**`, no de `apps/web/src/**`— y admiten
|
|
108
|
+
`*` dentro de un segmento, `**` cruzando segmentos y `?` por un carácter.
|
|
109
|
+
|
|
110
|
+
**Sin `scope` declarado no cambia nada**: cualquier delta sigue forzando la copia, que es el
|
|
111
|
+
comportamiento de siempre. El campo es opcional y no hay que adoptarlo.
|
|
112
|
+
|
|
113
|
+
Lo que se gana no es tiempo. Dentro de la copia `node_modules` viaja por **enlace**, y eso rompe
|
|
114
|
+
cualquier build de Turbopack sin salida del lado del proyecto: ahí un archivo ajeno al commit no cuesta
|
|
115
|
+
segundos, deja el gate sin poder pasar. Declarar el alcance es lo que lo destraba.
|
|
116
|
+
|
|
117
|
+
Dos bordes que valen la pena saber. Un directorio sin trackear llega colapsado —git reporta `extra/` y
|
|
118
|
+
no dice qué hay adentro—, así que se materializa igual si algún patrón apunta hacia adentro de él. Y
|
|
119
|
+
una ruta que no cuelga de **ninguna** raíz declarada también cuenta: puede ser de la instancia o de un
|
|
120
|
+
servicio que nadie declaró, y suponer que no importa es justo lo que este campo existe para evitar.
|
|
121
|
+
|
|
122
|
+
### Corregido
|
|
123
|
+
|
|
124
|
+
- **El motor se encuentra aunque lo hayas instalado un nivel arriba de tu instancia.** Si corrés
|
|
125
|
+
`npm install @ingeniomaps/cauce` en la carpeta de tu empresa y después `cauce init ops`, la instancia
|
|
126
|
+
queda adentro y el motor arriba. Ese es el árbol que el propio comando sugiere, y hasta ahora
|
|
127
|
+
`automation check` devolvía **nueve errores** sobre un motor que estaba instalado, cada uno mandándote a
|
|
128
|
+
correr `npm install` otra vez un nivel más abajo — o sea a bajar una segunda copia del paquete.
|
|
129
|
+
|
|
130
|
+
Ahora se busca también en la raíz que tu `ops.config.json` declara, que es la misma que el motor ya usa
|
|
131
|
+
para decidir dónde instalar el runner. Si tu `<empresa>-ops` tiene su propio `node_modules`, ése sigue
|
|
132
|
+
ganando y nada cambia.
|
|
133
|
+
|
|
134
|
+
**No se adivina el layout, se lee el declarado**: no se sube por el árbol como hace Node —en un monorepo
|
|
135
|
+
podría encontrar un motor de otra versión, en silencio— ni se mira si hay repositorio git, porque tener
|
|
136
|
+
la instancia sin versionar es un uso legítimo.
|
|
137
|
+
|
|
138
|
+
Vale para los tres caminos, no sólo para el CLI: el shim que lanza cada guard y el bridge de Antigravity
|
|
139
|
+
repiten esa búsqueda porque corren antes de poder cargar el motor, y los tres se actualizaron juntos.
|
|
140
|
+
|
|
17
141
|
## [0.91.0] - 2026-09-15
|
|
18
142
|
|
|
19
143
|
### Agregado
|
|
@@ -14,12 +14,12 @@ if [ -z "$hook_name" ]; then
|
|
|
14
14
|
exit 2
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
# antes de poder cargar el motor: si cambia allá, cambia acá.
|
|
17
|
+
# Copia de `packagePath`; su porqué vive allá. Son tres los que la repiten y el motor los nombra.
|
|
19
18
|
runner=""
|
|
20
19
|
for candidate in \
|
|
21
20
|
"$ops_root/node_modules/@ingeniomaps/cauce/engine/hooks/run.js" \
|
|
22
|
-
"$ops_root/engine/hooks/run.js"
|
|
21
|
+
"$ops_root/engine/hooks/run.js" \
|
|
22
|
+
"$ops_root/../node_modules/@ingeniomaps/cauce/engine/hooks/run.js"
|
|
23
23
|
do
|
|
24
24
|
if [ -f "$candidate" ]; then runner="$candidate"; break; fi
|
|
25
25
|
done
|
|
@@ -27,7 +27,7 @@ done
|
|
|
27
27
|
# Un guard que no encuentra su motor bloquea, nunca permite.
|
|
28
28
|
if [ -z "$runner" ]; then
|
|
29
29
|
echo "BLOQUEADO [$hook_name]: no se encontró el motor de hooks de Cauce." >&2
|
|
30
|
-
echo " Buscado en node_modules/@ingeniomaps/cauce y engine/ bajo $ops_root" >&2
|
|
30
|
+
echo " Buscado en node_modules/@ingeniomaps/cauce y engine/ bajo $ops_root y su carpeta padre" >&2
|
|
31
31
|
exit 2
|
|
32
32
|
fi
|
|
33
33
|
|
|
@@ -82,9 +82,9 @@ function runtimeAt(root) {
|
|
|
82
82
|
const candidates = [
|
|
83
83
|
path.join(root, 'node_modules', '@ingeniomaps', 'cauce', 'engine', 'hooks', 'run.js'),
|
|
84
84
|
path.join(root, 'engine', 'hooks', 'run.js'),
|
|
85
|
+
path.join(root, '..', 'node_modules', '@ingeniomaps', 'cauce', 'engine', 'hooks', 'run.js'),
|
|
85
86
|
]
|
|
86
|
-
//
|
|
87
|
-
// engine/core/ownership.js en vez de requerirla. Si cambia una, cambian las dos.
|
|
87
|
+
// Copia de `packagePath`; su porqué vive allá. Son tres los que la repiten y el motor los nombra.
|
|
88
88
|
const runtime = candidates.find(fs.existsSync)
|
|
89
89
|
if (!runtime) throw new Error('No se encontró el runtime engine/hooks/run.js.')
|
|
90
90
|
return require(runtime)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// misma razón por la que nadie corrige su propio examen.
|
|
6
6
|
//
|
|
7
7
|
// Dónde trabaja el cargo lo decide el modo: en el toolkit, un banco desechable por caso; en una
|
|
8
|
-
// empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/
|
|
8
|
+
// empresa, su propia instancia. El porqué del banco está en `evaluationBench` (engine/cli/bench.js).
|
|
9
9
|
// El veredicto, en cambio, se escribe siempre junto al cargo: el banco se borra, el contrato queda.
|
|
10
10
|
export const meta = {
|
|
11
11
|
name: 'agent-eval',
|
|
@@ -36,7 +36,6 @@ const BACKLOG = `${P}/BACKLOG.md`
|
|
|
36
36
|
const doneFile = (slug) => `${P}/done/${slug}.md`
|
|
37
37
|
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
38
38
|
const GATE = `${P}/AWAITING_REVIEW.md`
|
|
39
|
-
const ROADMAP = `${P}/roadmap`
|
|
40
39
|
|
|
41
40
|
// Estado de planning tal como lo emite `ops context --json`; ningún modelo parsea BACKLOG ni WIP.
|
|
42
41
|
// De a pares, y sin regex: una comilla dentro de un literal de regex desincroniza a las dos puertas que
|
|
@@ -87,6 +86,21 @@ const CONTEXT = {
|
|
|
87
86
|
inbox: { ...INBOX_HEADS },
|
|
88
87
|
// Las reglas que rigen el proyecto, con los overrides ya resueltos por el motor (caso 105).
|
|
89
88
|
rules: { type: 'array', items: { type: 'string' } },
|
|
89
|
+
// Cuántos pasos del plan están tildados y cuántos no. Es lo único que separa «esta tarea viene de una
|
|
90
|
+
// corrida que paró a mitad» de «esta tarea no empezó», y sin eso Build se lanzaba igual con los nueve
|
|
91
|
+
// pasos hechos: el agente releía el WIP, miraba el disco y contestaba que no había nada pendiente —
|
|
92
|
+
// medido en 893.000 tokens sobre tres corridas de una sola tarea (caso 154).
|
|
93
|
+
//
|
|
94
|
+
// Viene de `ops context --json`, que ya lo emite; acá sólo hacía falta declararlo, porque
|
|
95
|
+
// `additionalProperties: false` lo descartaba aunque llegara. Es opcional: una instancia sin WIP
|
|
96
|
+
// activo no lo trae, y pedirlo siempre obligaría a inventar ceros donde no hay plan.
|
|
97
|
+
wip: {
|
|
98
|
+
type: 'object', additionalProperties: false, required: ['complete', 'pending'],
|
|
99
|
+
properties: {
|
|
100
|
+
phase: { type: 'string' },
|
|
101
|
+
complete: { type: 'integer' }, pending: { type: 'integer' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
90
104
|
},
|
|
91
105
|
}
|
|
92
106
|
const CLAIM = {
|
|
@@ -290,6 +304,22 @@ const VERDICT = ' Cerrá con verdict=aprobado si no queda nada por corregir ante
|
|
|
290
304
|
const RULED = ' En rules nombrá, por su ruta, cada una de las reglas que rigen contra la que revisaste el diff.'
|
|
291
305
|
// Lo que hay que corregir antes de entregar. El resto de los hallazgos no desaparece: se registra.
|
|
292
306
|
const blockers = (verdict) => verdict.concerns.filter((one) => one.blocking).map((one) => one.detail)
|
|
307
|
+
// El resultado de Build cuando no hubo nada que construir en esta corrida. Devuelve lo que de verdad
|
|
308
|
+
// pasó y nada más: `redFirst` y `discovered` van **vacíos** porque acá no hubo ningún rojo nuevo que
|
|
309
|
+
// mostrar ni ningún borde nuevo que fijar, y rellenarlos para que se parezca a una construcción sería
|
|
310
|
+
// fabricar la evidencia que este recorrido exige justamente para no tener que creerle a nadie.
|
|
311
|
+
//
|
|
312
|
+
// `completed: true` afirma que el plan no tiene pasos pendientes, que es lo que el WIP dice y lo único
|
|
313
|
+
// que se está usando. No afirma que lo construido esté bien: eso lo miran Review, Verify y QA sobre el
|
|
314
|
+
// diff real, que existe en disco venga de la corrida que venga.
|
|
315
|
+
const reusedBuild = (wip) => ({
|
|
316
|
+
completed: true,
|
|
317
|
+
closedTask: false,
|
|
318
|
+
redFirst: [],
|
|
319
|
+
discovered: [],
|
|
320
|
+
summary: `sin construir en esta corrida: el WIP traía ${wip.complete} paso(s) tildado(s) y ninguno `
|
|
321
|
+
+ 'pendiente, así que lo que sigue revisa lo que ya estaba en disco',
|
|
322
|
+
})
|
|
293
323
|
// Atajo para reconocer un gate que corrió pruebas sin preguntarle a nadie. No alcanza solo y no
|
|
294
324
|
// pretende hacerlo: `mvn verify`, `gradle build`, `tox`, `bin/rails t` y cualquier `make` con nombre
|
|
295
325
|
// propio corren pruebas y no se parecen a esto, así que el que corrió el comando además lo declara en
|
|
@@ -389,7 +419,8 @@ const registerHuman = async (prompt, label) => (await write(prompt, { label })
|
|
|
389
419
|
const readContext = () => read(
|
|
390
420
|
`Corré "node tools/ops.js context ${P} --json" desde ${ROOT} y reportá sólo lo que imprimió. Derivá hasTask ` +
|
|
391
421
|
`de si task es null, wipActive de si wip es null, claimed del campo claimed, today y wipFile de sus ` +
|
|
392
|
-
`campos, rules del campo rules tal cual, y
|
|
422
|
+
`campos, rules del campo rules tal cual, wip con sus campos complete y pending tal cual si viene —y ` +
|
|
423
|
+
`omitilo entero si wip es null, sin inventar ceros—, y lane ` +
|
|
393
424
|
`de task.tier; copiá slug, ` +
|
|
394
425
|
`hito, service, acceptance, ` +
|
|
395
426
|
`epic y cast de task, epicContext de epic.context —vacío si no hay épica— e inbox tal cual. El comando es ` +
|
|
@@ -710,8 +741,25 @@ while (rounds++ < MAX_TASKS) {
|
|
|
710
741
|
}
|
|
711
742
|
}
|
|
712
743
|
|
|
713
|
-
|
|
714
|
-
|
|
744
|
+
// Un WIP que viene de una corrida anterior con todos sus pasos tildados no tiene nada que construir, y
|
|
745
|
+
// lanzar el agente para que lo confirme cuesta lo mismo que construir. El recorrido delegaba la
|
|
746
|
+
// reanudación en el prompt —«retomá en el primer paso pendiente»—, así que el agente hacía lo correcto
|
|
747
|
+
// y lo caro era haberlo llamado (caso 154).
|
|
748
|
+
//
|
|
749
|
+
// **La fase no se saltea: se saltea la llamada.** Los cuatro contrastes de abajo —la tarea cerrada en
|
|
750
|
+
// Build, el rojo sin su fallo literal, el borde sin prueba, las decisiones abiertas— son lo único que
|
|
751
|
+
// vuelve a mirar el disco, y darlos por buenos porque el WIP dice que está todo hecho camina al modo de
|
|
752
|
+
// fallo que registra la fase WIP acá arriba: alguien construyó todo y Review, Verify y QA no lo vieron.
|
|
753
|
+
// Por eso lo que sigue no afirma que la construcción estuvo bien, sólo que en **esta** corrida no hubo
|
|
754
|
+
// ninguna: lo que ya estaba en disco lo revisan igual las fases siguientes, sobre el diff real.
|
|
755
|
+
// Y la fase se anuncia distinto, que es la otra mitad del caso: hoy «Build corrió y construyó» y «Build
|
|
756
|
+
// corrió, miró y no hizo nada» se ven idénticos salvo por el costo, así que R21 —que manda comprobar la
|
|
757
|
+
// reanudación en vez de suponerla— no se puede cumplir sobre este recorrido sin abrir el `.output` y
|
|
758
|
+
// sumar tokens a mano. El nombre viaja por el mismo canal que las otras quince fases: entra en `ran`,
|
|
759
|
+
// que va al resultado de la corrida y a la entrada de DONE.
|
|
760
|
+
const resumed = planning.wip && planning.wip.pending === 0 && planning.wip.complete > 0
|
|
761
|
+
phase(resumed ? 'Build (reanudado)' : 'Build')
|
|
762
|
+
const build = resumed ? reusedBuild(planning.wip) : await run(
|
|
715
763
|
`${asRole(cast.build)}Implementá sólo ${task.id} dentro de ${task.service}. Retomá en el primer paso ` +
|
|
716
764
|
`pendiente del WIP; comprobá en el disco los pasos ya hechos y tildá cada uno que salga bien. Para cada ` +
|
|
717
765
|
`comportamiento escribí primero la prueba, corréla y anotá en redFirst el test y el fallo literal que ` +
|
|
@@ -352,8 +352,6 @@ function prepareProposal(root, agent, now = new Date(), period = '', kind = 'age
|
|
|
352
352
|
const reportDir = path.join(target, 'learning', 'reports')
|
|
353
353
|
const reports = pendingReports(target, sealing)
|
|
354
354
|
const red = verdictFindings(root, target)
|
|
355
|
-
if (!reports.length && !red.findings.length) return { file: '', created: false, reports: 0 }
|
|
356
|
-
|
|
357
355
|
const reportPaths = reports.map((name) => path.join(reportDir, name))
|
|
358
356
|
// La misma regla que la rama de recorridos, por el mismo motivo: un documento que no puede decir qué
|
|
359
357
|
// corregir no cambia ningún contrato y cuesta igual la firma humana que uno que sí. Un informe puede
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Qué le falta a la superficie de automatización de una instancia, y nada más. Se mira sin tocar: `check`
|
|
4
|
+
// enumera y devuelve; quien decide qué hacer con esa lista es el CLI.
|
|
5
|
+
//
|
|
6
|
+
// Vive aparte de `index.js` porque no comparte nada con los otros tres verbos. Medido antes de partir:
|
|
7
|
+
// `doctor`, `install` y `uninstall` se apoyan en los mismos ayudantes —`deliveryKey`, `deliveryState`,
|
|
8
|
+
// `removeFile`, `probeBridge`—, y este par no toca ninguno. Lo único que comparte son los imports, que es
|
|
9
|
+
// lo que comparte cualquier archivo del directorio.
|
|
10
|
+
//
|
|
11
|
+
// La partición estaba anotada como deuda desde que el archivo cruzó las 500 líneas con el aviso de
|
|
12
|
+
// sidecar del caso 138, estando en 499.
|
|
13
|
+
|
|
14
|
+
const fs = require('node:fs')
|
|
15
|
+
const path = require('node:path')
|
|
16
|
+
const O = require('../core/ownership')
|
|
17
|
+
const {
|
|
18
|
+
RUNNER_NAMES, packagedAutomation, runnerManifest, runnerPaths, resolveItem, runnerConfig,
|
|
19
|
+
} = require('./runners')
|
|
20
|
+
const { expectedHooks, staleHooks } = require('./hooks')
|
|
21
|
+
const { hasHooks } = require('./config')
|
|
22
|
+
|
|
23
|
+
function check(root) {
|
|
24
|
+
const errors = []
|
|
25
|
+
const hookDir = path.join(root, 'automatization', 'hooks')
|
|
26
|
+
if (!fs.existsSync(path.join(root, 'automatization', 'AGENTS.md'))) {
|
|
27
|
+
errors.push('falta automatization/AGENTS.md')
|
|
28
|
+
}
|
|
29
|
+
for (const name of expectedHooks()) {
|
|
30
|
+
const file = path.join(hookDir, name)
|
|
31
|
+
if (!fs.existsSync(file)) errors.push(`falta automatization/hooks/${name}`)
|
|
32
|
+
else if (!(fs.statSync(file).mode & 0o111)) {
|
|
33
|
+
errors.push(`automatization/hooks/${name} no es ejecutable`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// El motor puede venir de la dependencia npm o del propio repositorio, y la cascada la resuelve
|
|
37
|
+
// `packagePath`. Eran tres: la copia vendorizada se retiró en 0.10.0 y esta línea la sobrevivió.
|
|
38
|
+
if (!O.engineAt(root, path.join('hooks', 'run.js'))) {
|
|
39
|
+
errors.push('falta engine/hooks/run.js: corré "npm install" en la raíz del repo ops')
|
|
40
|
+
}
|
|
41
|
+
const workflows = [
|
|
42
|
+
'autobuild.js',
|
|
43
|
+
'flow.js',
|
|
44
|
+
path.join('integrations', 'sync.js'),
|
|
45
|
+
path.join('integrations', 'promote.js'),
|
|
46
|
+
]
|
|
47
|
+
const packaged = packagedAutomation(root)
|
|
48
|
+
for (const name of workflows) {
|
|
49
|
+
if (!packaged || !fs.existsSync(path.join(packaged, 'workflows', name))) {
|
|
50
|
+
errors.push(`falta automatization/workflows/${name}: corré "npm install" en la raíz del repo ops`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Un choque que `upgrade` conservó (caso 110) también queda distinto del paquete, y mandarlo a correr
|
|
54
|
+
// `upgrade` era una vuelta sin salida: lo conservaría otra vez. Se dice qué es y qué hacer.
|
|
55
|
+
const choques = new Set(O.collisions(root))
|
|
56
|
+
for (const { file, edited } of staleHooks(root)) {
|
|
57
|
+
if (choques.has(`automatization/hooks/${file}`)) {
|
|
58
|
+
errors.push(`automatization/hooks/${file}: es tuyo y se llama como uno que trae el paquete, así que el `
|
|
59
|
+
+ "del paquete no está instalado; renombrá el tuyo y corré `cauce upgrade`")
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
errors.push(edited
|
|
63
|
+
? `automatization/hooks/${file}: lo editaste y es del toolkit; agregá un guard propio al lado `
|
|
64
|
+
+ 'o descartá tu cambio con `cauce upgrade --force`'
|
|
65
|
+
: `automatization/hooks/${file}: quedó atrás del paquete y ya no protege lo que dice; `
|
|
66
|
+
+ 'corré `cauce upgrade` antes de instalar el runner')
|
|
67
|
+
}
|
|
68
|
+
for (const name of RUNNER_NAMES) validateRunnerManifest(root, name, errors)
|
|
69
|
+
return errors
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateRunnerManifest(root, name, errors) {
|
|
73
|
+
try {
|
|
74
|
+
const runner = runnerManifest(root, name)
|
|
75
|
+
if (runner.name !== name || runner.schemaVersion !== 1
|
|
76
|
+
|| !runner.config || !runner.capabilities) {
|
|
77
|
+
errors.push(`${name}: manifest incompleto`)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const paths = runnerPaths(root, name, runner)
|
|
81
|
+
const config = runnerConfig(paths, root)
|
|
82
|
+
if (runner.capabilities.nativeHooks && !hasHooks(config)) {
|
|
83
|
+
errors.push(`${name}: declara hooks nativos pero no los configura`)
|
|
84
|
+
}
|
|
85
|
+
for (const item of [...(runner.instructions || []), ...(runner.artifacts || [])]) {
|
|
86
|
+
const resolved = resolveItem(paths, root, name, item)
|
|
87
|
+
if (!fs.existsSync(resolved.source)) errors.push(`${name}: falta ${item.source}`)
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
errors.push(`${name}: configuración inválida (${error.message})`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
module.exports = { check }
|
|
@@ -9,89 +9,22 @@ const O = require('../core/ownership')
|
|
|
9
9
|
const M = require('../core/manifest')
|
|
10
10
|
const RL = require('./rules')
|
|
11
11
|
const {
|
|
12
|
-
RUNNER_NAMES, OPS_DIR, OPS_ROOT,
|
|
12
|
+
RUNNER_NAMES, OPS_DIR, OPS_ROOT, runnerManifest, installRoot, opsPrefix,
|
|
13
13
|
runnerPaths, resolveItem, inline, render, runnerConfig, activated,
|
|
14
14
|
} = require('./runners')
|
|
15
15
|
const { roleCatalog, roleSkill, installRoleSkills } = require('./roles')
|
|
16
16
|
const {
|
|
17
|
-
GUARD_NAMES, groupWrappers,
|
|
18
|
-
legacyGuardWiring,
|
|
17
|
+
GUARD_NAMES, groupWrappers, supersededGuards,
|
|
18
|
+
legacyGuardWiring, listHooks,
|
|
19
19
|
} = require('./hooks')
|
|
20
20
|
const {
|
|
21
|
-
blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig,
|
|
21
|
+
blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig,
|
|
22
22
|
unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
|
|
23
23
|
} = require('./config')
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!fs.existsSync(path.join(root, 'automatization', 'AGENTS.md'))) {
|
|
29
|
-
errors.push('falta automatization/AGENTS.md')
|
|
30
|
-
}
|
|
31
|
-
for (const name of expectedHooks()) {
|
|
32
|
-
const file = path.join(hookDir, name)
|
|
33
|
-
if (!fs.existsSync(file)) errors.push(`falta automatization/hooks/${name}`)
|
|
34
|
-
else if (!(fs.statSync(file).mode & 0o111)) {
|
|
35
|
-
errors.push(`automatization/hooks/${name} no es ejecutable`)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
// El motor puede venir de la dependencia npm o del propio repositorio, y la cascada la resuelve
|
|
39
|
-
// `packagePath`. Eran tres: la copia vendorizada se retiró en 0.10.0 y esta línea la sobrevivió.
|
|
40
|
-
if (!O.engineAt(root, path.join('hooks', 'run.js'))) {
|
|
41
|
-
errors.push('falta engine/hooks/run.js: corré "npm install" en la raíz del repo ops')
|
|
42
|
-
}
|
|
43
|
-
const workflows = [
|
|
44
|
-
'autobuild.js',
|
|
45
|
-
'flow.js',
|
|
46
|
-
path.join('integrations', 'sync.js'),
|
|
47
|
-
path.join('integrations', 'promote.js'),
|
|
48
|
-
]
|
|
49
|
-
const packaged = packagedAutomation(root)
|
|
50
|
-
for (const name of workflows) {
|
|
51
|
-
if (!packaged || !fs.existsSync(path.join(packaged, 'workflows', name))) {
|
|
52
|
-
errors.push(`falta automatization/workflows/${name}: corré "npm install" en la raíz del repo ops`)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
// Un choque que `upgrade` conservó (caso 110) también queda distinto del paquete, y mandarlo a correr
|
|
56
|
-
// `upgrade` era una vuelta sin salida: lo conservaría otra vez. Se dice qué es y qué hacer.
|
|
57
|
-
const choques = new Set(O.collisions(root))
|
|
58
|
-
for (const { file, edited } of staleHooks(root)) {
|
|
59
|
-
if (choques.has(`automatization/hooks/${file}`)) {
|
|
60
|
-
errors.push(`automatization/hooks/${file}: es tuyo y se llama como uno que trae el paquete, así que el `
|
|
61
|
-
+ "del paquete no está instalado; renombrá el tuyo y corré `cauce upgrade`")
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
errors.push(edited
|
|
65
|
-
? `automatization/hooks/${file}: lo editaste y es del toolkit; agregá un guard propio al lado `
|
|
66
|
-
+ 'o descartá tu cambio con `cauce upgrade --force`'
|
|
67
|
-
: `automatization/hooks/${file}: quedó atrás del paquete y ya no protege lo que dice; `
|
|
68
|
-
+ 'corré `cauce upgrade` antes de instalar el runner')
|
|
69
|
-
}
|
|
70
|
-
for (const name of RUNNER_NAMES) validateRunnerManifest(root, name, errors)
|
|
71
|
-
return errors
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function validateRunnerManifest(root, name, errors) {
|
|
75
|
-
try {
|
|
76
|
-
const runner = runnerManifest(root, name)
|
|
77
|
-
if (runner.name !== name || runner.schemaVersion !== 1
|
|
78
|
-
|| !runner.config || !runner.capabilities) {
|
|
79
|
-
errors.push(`${name}: manifest incompleto`)
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
const paths = runnerPaths(root, name, runner)
|
|
83
|
-
const config = runnerConfig(paths, root)
|
|
84
|
-
if (runner.capabilities.nativeHooks && !hasHooks(config)) {
|
|
85
|
-
errors.push(`${name}: declara hooks nativos pero no los configura`)
|
|
86
|
-
}
|
|
87
|
-
for (const item of [...(runner.instructions || []), ...(runner.artifacts || [])]) {
|
|
88
|
-
const resolved = resolveItem(paths, root, name, item)
|
|
89
|
-
if (!fs.existsSync(resolved.source)) errors.push(`${name}: falta ${item.source}`)
|
|
90
|
-
}
|
|
91
|
-
} catch (error) {
|
|
92
|
-
errors.push(`${name}: configuración inválida (${error.message})`)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
25
|
+
// Qué le falta a la superficie de automatización, que no comparte ayudantes con los tres verbos que
|
|
26
|
+
// escriben. Se reexporta para que sus consumidores sigan pidiéndoselo a este módulo.
|
|
27
|
+
const { check } = require('./check')
|
|
95
28
|
|
|
96
29
|
// Ejecuta el puente del runner tal como él lo invoca, y desde otra carpeta. Instalado no es lo mismo que
|
|
97
30
|
// operativo: un bridge que el runner no puede lanzar —porque su ruta es relativa y el cwd es otro, o
|