@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 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
- # La misma cascada que `packagePath` en engine/core/ownership.js, repetida acá porque el shim corre
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
- // El bridge corre antes de poder cargar el motor, así que repite la cascada de
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/catalog.js).
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 lane ` +
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
- phase('Build')
714
- const build = await run(
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, packagedAutomation, runnerManifest, installRoot, opsPrefix,
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, expectedHooks, supersededGuards,
18
- legacyGuardWiring, staleHooks, listHooks,
17
+ GUARD_NAMES, groupWrappers, supersededGuards,
18
+ legacyGuardWiring, listHooks,
19
19
  } = require('./hooks')
20
20
  const {
21
- blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
21
+ blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig,
22
22
  unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
23
23
  } = require('./config')
24
24
 
25
- function check(root) {
26
- const errors = []
27
- const hookDir = path.join(root, 'automatization', 'hooks')
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
@@ -21,6 +21,7 @@ const FLAGS = {
21
21
  tree: ['--json', '--no-color'],
22
22
  context: ['--json', '--hito'],
23
23
  contract: ['--json'],
24
+ bench: ['--force'],
24
25
  recurring: ['--json', '--promote'],
25
26
  claim: ['--json'],
26
27
  runners: ['--json'],