@ingeniomaps/cauce 0.92.0 → 0.94.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 +134 -0
- package/automatization/workflows/autobuild.js +0 -1
- package/engine/agents/learning.js +0 -2
- package/engine/automation/check.js +95 -0
- package/engine/automation/index.js +7 -74
- package/engine/automation/rules.js +27 -9
- package/engine/cli/contract.js +37 -10
- package/engine/hooks/run.js +2 -1
- package/engine/hooks/shell.js +9 -262
- package/engine/hooks/verify.js +275 -0
- package/package.json +1 -1
- package/template/planning/rules/README.md +8 -5
- package/template/planning/rules/system/code-shape.md +19 -0
- package/template/planning/rules/system/commits.md +35 -0
- package/template/planning/rules/system/conduct.md +23 -0
- package/template/planning/rules/system/process.md +48 -118
- package/template/planning/rules/system/runs.md +153 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,140 @@ 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.94.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **Cinco reglas nuevas, traídas de una empresa que las pagó.** Salieron de revisar las reglas propias de
|
|
22
|
+
una instancia real: no son ideas, cada una tiene adentro la corrida que costó.
|
|
23
|
+
|
|
24
|
+
- **R24 — una premisa sobre el propio código se abre antes de usarla.** R14 ya exigía registro para lo
|
|
25
|
+
que se afirma de una herramienta o una norma, y dejaba afuera tu propio repositorio, que es donde
|
|
26
|
+
nadie te va a discutir. Cuatro corridas perdidas en un día, todas frenadas en la puerta y ninguna por
|
|
27
|
+
el código: la aceptación pedía algo que el sistema no hace. Y el ancla `archivo:línea` se abre, no se
|
|
28
|
+
copia — una función se movió de la 555 a la 733 en la misma sesión.
|
|
29
|
+
- **R25 — el identificador de una unidad de trabajo no cambia mientras está viva.** Renombrar un slug a
|
|
30
|
+
mitad de camino rompe el cruce entre la cola y lo hecho **sin que nada falle**: cada lado se lee
|
|
31
|
+
coherente por separado. Una tarea partida en cuatro y cerrada con otros nombres costó 594k tokens de
|
|
32
|
+
la corrida siguiente para descubrir que ya estaba construida. Si el nombre tiene que cambiar, va
|
|
33
|
+
`slug-nuevo (antes: slug-viejo)` hasta cerrar.
|
|
34
|
+
- **R26 — una puerta acota su propio costo y no escribe en el árbol que juzga.** Dos revisores lanzando
|
|
35
|
+
la misma suite fueron cuatro corridas en cuatro minutos: el sistema operativo mató la sesión entera
|
|
36
|
+
con un pico de 24,2 GB. Y un formateador con `--fix` o un build que limpia su salida editan el trabajo
|
|
37
|
+
de quien está commiteando. Una puerta que estorba se saltea, y desde ahí no protege de nada.
|
|
38
|
+
- **R27 — una defensa se aplica por defecto y cada excepción se declara sola.** Con lista de lo que
|
|
39
|
+
protege, todo lo que se agregue después nace afuera y nada lo compara. Incluye el caso que más se
|
|
40
|
+
disfraza: «esta comprobación no corre en desarrollo» es una quita escrita como agregado, y garantiza
|
|
41
|
+
que el camino de producción sea el único que nunca se ejercitó.
|
|
42
|
+
- **R28 — un estado lo dice el contenido de un archivo, nunca su presencia.** Un centinela cuya única
|
|
43
|
+
información es existir obliga a que borrarlo sea parte de la resolución, y eso alguien lo olvida: la
|
|
44
|
+
corrida arranca, lee todo el estado y recién ahí muere. Pasó dos veces el mismo día, a 42k tokens por
|
|
45
|
+
vez. Es lo que el WIP de Cauce ya hace bien con `status: IDLE`.
|
|
46
|
+
|
|
47
|
+
- **R9 dice cuándo se puede quitar lo que está en uso.** Exigía probar la ausencia de lo quitado y nunca
|
|
48
|
+
decía cuándo se puede quitar. Ahora: lo que está en uso no se corta, se depreca, y la marca dice las
|
|
49
|
+
dos cosas que la vuelven una salida y no una etiqueta — qué lo reemplaza, y qué condición permite
|
|
50
|
+
borrarlo. Sin la primera, quien lo usa no sabe a dónde ir; sin la segunda, el deprecado es código
|
|
51
|
+
muerto con un cartel puesto y se queda para siempre.
|
|
52
|
+
|
|
53
|
+
Y lo que no llama nadie es otra cosa: se borra. Cortar de golpe rompe a un consumidor que nadie miró;
|
|
54
|
+
deprecar lo que nadie usa cuesta mantener dos caminos para nadie.
|
|
55
|
+
|
|
56
|
+
- **R10 dice a dónde va lo que se publica, no sólo quién lo autoriza.** La autorización decía si se
|
|
57
|
+
publica y nunca dónde. Ahora: lo que se publica va al repositorio en el que estás trabajando, y si ese
|
|
58
|
+
remoto es un fork, va al fork — con la rama cortada de la suya, porque una rama cortada del principal
|
|
59
|
+
es la antesala de mandarle el PR.
|
|
60
|
+
|
|
61
|
+
No se deduce del contexto: que la herramienta resuelva sola el repositorio de origen no es una
|
|
62
|
+
autorización, ni lo son que el cambio «obviamente tenga que llegar ahí» ni que un PR anterior haya ido
|
|
63
|
+
a parar allá. Saltar al principal se pide con todas las letras y para ese caso concreto.
|
|
64
|
+
|
|
65
|
+
Es de las pocas sin vuelta atrás: un PR mal apuntado es trabajo publicado en el repositorio de otro
|
|
66
|
+
equipo — lo vieron, les llegó la notificación, y cerrarlo no deshace nada de eso.
|
|
67
|
+
|
|
68
|
+
- **R8 dice que la prohibición de firmas de IA cubre todo lo que se publica**, no sólo el mensaje del
|
|
69
|
+
commit: el título y el cuerpo del pull request, y los comentarios que se dejen ahí. Y casi nunca es
|
|
70
|
+
algo que alguien tipea — lo agrega la herramienta sola, al final del texto que escribiste—, así que
|
|
71
|
+
cumplirla es revisar la salida antes de publicarla, no acordarse de no escribirla.
|
|
72
|
+
|
|
73
|
+
- **R17 dice qué cuenta como una condición, que es lo que volvía incontable su umbral.** La barra son
|
|
74
|
+
cinco condiciones de aceptación y nunca decía qué es una. Ahora: una condición es un resultado que se
|
|
75
|
+
puede mirar por separado, no una viñeta. Cinco viñetas que describen el mismo invariante desde cinco
|
|
76
|
+
ángulos son **una**, y contarlas como cinco parte por la mitad lo que era una sola cosa.
|
|
77
|
+
|
|
78
|
+
La otra dirección es la cara y la que nadie mira: una frase que promete dos resultados con vidas
|
|
79
|
+
distintas —«valida el pago y manda el email»— son **dos**, y escrita como una el umbral no se entera
|
|
80
|
+
nunca. Contar de menos no dispara nada, y se lee igual que una unidad chica.
|
|
81
|
+
|
|
82
|
+
La prueba no pide criterio: si al tachar una condición las otras siguen valiendo, son distintas; si
|
|
83
|
+
tachar una deja a las demás sin sentido, era una sola dicha en partes.
|
|
84
|
+
|
|
85
|
+
- **R9 ahora pide que la mutación quede escrita, no sólo que se corra.** R9 ya exigía romper, con el
|
|
86
|
+
código puesto, exactamente lo que el caso dice cuidar, y verlo ponerse rojo. Lo que faltaba es que eso
|
|
87
|
+
quedara en la aceptación: una línea con qué se rompe y qué prueba tiene que ponerse roja. Sin ella,
|
|
88
|
+
quien revisa no puede distinguir la mutación que se corrió de la que se pensó, y lo único que le queda
|
|
89
|
+
es volver a correrla — o sea rehacer el trabajo que delegarlo evitaba.
|
|
90
|
+
|
|
91
|
+
Y escribirla antes cambia lo que se escribe: una aceptación que tiene que nombrar qué romper deja de
|
|
92
|
+
poder pedir algo que ninguna mutación puede tocar. Si no hay nada que romper, no había propiedad que
|
|
93
|
+
cuidar, y eso se ve al redactarla en vez de al final de la vuelta.
|
|
94
|
+
|
|
95
|
+
**Lo que cuesta:** el bloque de reglas que cada agente carga al arrancar pasa de **39,1 a 49,1 KB**.
|
|
96
|
+
Está medido, no estimado, y el umbral del aviso de `check` **no se movió**: sigue en 64 KB, porque lo
|
|
97
|
+
que mide es cuánto agregaste vos, y subirlo para hacerle lugar al piso apagaría justamente eso. Quedan
|
|
98
|
+
~18 KB de margen antes de que el aviso hable.
|
|
99
|
+
|
|
100
|
+
### Corregido
|
|
101
|
+
|
|
102
|
+
- **El aviso de peso mide lo que agregaste vos, no el total.** Comparaba el bloque entero contra el
|
|
103
|
+
umbral, así que el piso del toolkit y tus reglas salían del mismo bolsillo: decía «tu bloque pesa»
|
|
104
|
+
cuando la mitad la habíamos puesto nosotros, y cada regla que Cauce agregaba te achicaba el margen sin
|
|
105
|
+
que nadie lo decidiera. Ahora el umbral se compara contra tus reglas, y la línea dice las dos cosas —
|
|
106
|
+
`114.8 KB en cada agente (93.8 KB propias)`—: el total es lo que paga el agente y lo propio es lo único
|
|
107
|
+
sobre lo que podés hacer algo.
|
|
108
|
+
|
|
109
|
+
**Una instancia recién creada ya no puede cruzarlo**, por más que el piso crezca. Antes era una cuenta
|
|
110
|
+
que había que rehacer cada vez que agregábamos una regla.
|
|
111
|
+
|
|
112
|
+
El número **no se movió**: sigue en 64 KB, a propósito, porque cambiar qué se mide y cuánto a la vez
|
|
113
|
+
deja sin saber cuál de los dos movió el resultado. Lo que sí quedó medido es que 64 está por debajo de
|
|
114
|
+
lo que una empresa real usa — una instancia medida tiene 93,8 KB de reglas propias — así que elegirlo
|
|
115
|
+
con esa evidencia es lo que sigue.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
- **Escribir tu propio `process.md` ya no te deja sin las reglas que no ibas a reemplazar.** El override
|
|
119
|
+
es por nombre de archivo, así que reemplazar «pensar antes de editar» por tu versión se llevaba puesto
|
|
120
|
+
el archivo entero: R16, R17, R20, R21 y R22 dejaban de llegarle a todo agente. `check` te lo decía —lo
|
|
121
|
+
hace desde 0.57.0— y no había nada que hacer al respecto, porque conservarlas exigía copiar su texto y
|
|
122
|
+
una copia deja de recibir las mejoras del `upgrade`.
|
|
123
|
+
|
|
124
|
+
Ahora **R16, R20, R21 y R22 viven en `system/runs.md`** —lo que cuesta una corrida, cuándo una medición
|
|
125
|
+
vale, cómo se retoma lo interrumpido y qué no se toca mientras se mide—, un archivo que reemplazar tu
|
|
126
|
+
proceso no toca. `system/process.md` se queda con R1..R4 y R17.
|
|
127
|
+
|
|
128
|
+
**No tenés que hacer nada**: el archivo nuevo llega en tu próximo `upgrade`. Si ya sobrescribiste
|
|
129
|
+
`process.md`, esas cuatro reglas vuelven a regir solas, y el aviso de `check` se acorta a lo que de
|
|
130
|
+
verdad reemplazaste. El bloque de reglas pasa de cuatro archivos a cinco y pesa lo mismo: 39,1 KB.
|
|
131
|
+
|
|
132
|
+
## [0.93.0] - 2026-09-16
|
|
133
|
+
|
|
134
|
+
### Corregido
|
|
135
|
+
|
|
136
|
+
- **Declarar un límite ahora lo saca del aviso, en vez de sumarlo.** Si escribiste tus límites como
|
|
137
|
+
viñetas bajo `### Límites` —el camino que 0.92.0 agregó—, `check` los contaba igual como párrafos que
|
|
138
|
+
no llegan a los agentes, y también contaba la frase con la que presentabas la lista. O sea que hacer
|
|
139
|
+
lo correcto **subía** el número: medido sobre un banco, de 4 párrafos avisados pasaba a 6.
|
|
140
|
+
|
|
141
|
+
Ahora una viñeta declarada y la prosa que la presenta no entran en el aviso. Lo que sigue entrando es
|
|
142
|
+
la prosa de afuera del bloque, que es lo único para lo que el aviso existe: descontar el bloque entero
|
|
143
|
+
la habría silenciado, porque a un bloque `### Límites` no lo cierra nada más que el próximo
|
|
144
|
+
encabezado y el del molde se extiende hasta donde escribas el tuyo.
|
|
145
|
+
|
|
146
|
+
- **El aviso te manda al camino declarado y no a imitar una gramática.** Decía que tus párrafos no
|
|
147
|
+
llegan «porque no arrancan con «El runner», «Debe» o «Nunca»». Desde 0.92.0 hay una forma de
|
|
148
|
+
arreglarlo sin imitar nada, y es la que el aviso nombra ahora: sumar el límite como viñeta bajo
|
|
149
|
+
`### Límites`. Los dos caminos siguen valiendo; lo que cambia es cuál se recomienda.
|
|
150
|
+
|
|
17
151
|
## [0.92.0] - 2026-09-15
|
|
18
152
|
|
|
19
153
|
### Agregado
|
|
@@ -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
|
|
@@ -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
|
|
@@ -67,38 +67,56 @@ function split(root) {
|
|
|
67
67
|
function weight(root) {
|
|
68
68
|
const { loaded } = split(root)
|
|
69
69
|
let bytes = 0
|
|
70
|
+
let own = 0
|
|
70
71
|
const files = []
|
|
71
72
|
for (const file of loaded) {
|
|
72
73
|
try {
|
|
73
74
|
const size = fs.statSync(path.join(root, file)).size
|
|
74
75
|
bytes += size
|
|
76
|
+
// Lo propio es lo que no vive en `rules/system/`, que es exactamente lo que el proyecto escribió:
|
|
77
|
+
// una regla del sistema que sobrescribió deja de cargarse y la suya ocupa su lugar, así que
|
|
78
|
+
// contarla como propia es correcto — la escribió él y la puede achicar.
|
|
79
|
+
if (!file.includes('/system/')) own += size
|
|
75
80
|
files.push({ file, size })
|
|
76
81
|
} catch { /* la que no está en disco ya la reporta `check` por su lado */ }
|
|
77
82
|
}
|
|
78
|
-
return { count: loaded.length, bytes, files: files.sort((a, b) => b.size - a.size) }
|
|
83
|
+
return { count: loaded.length, bytes, own, files: files.sort((a, b) => b.size - a.size) }
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
const KB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`
|
|
82
87
|
|
|
83
|
-
// A partir de dónde el
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
+
// A partir de dónde lo que el proyecto agregó deja de ser el costo de arrancar y pasa a ser una decisión
|
|
89
|
+
// que conviene mirar. **Se compara contra lo propio y no contra el total**, y esa es la diferencia que
|
|
90
|
+
// hace al número significar algo.
|
|
91
|
+
//
|
|
92
|
+
// Contra el total, el piso del toolkit y las reglas de la empresa salían del mismo bolsillo: el aviso
|
|
93
|
+
// decía «tu bloque pesa» cuando la mitad la habíamos puesto nosotros, y cada regla que Cauce agregaba le
|
|
94
|
+
// achicaba el margen sin que nadie lo decidiera. El 64 tampoco salió de un costo medido: salió de
|
|
95
|
+
// esquivar nuestro propio piso —el caso 141 proponía 60 y se subió porque lo que Cauce ponía ya eran
|
|
96
|
+
// 61,9 KB—, así que había que reelegirlo cada vez que el toolkit enseñaba algo. Medido sobre lo propio,
|
|
97
|
+
// el número deja de depender de nosotros y no se toca cuando el piso crece.
|
|
98
|
+
//
|
|
99
|
+
// El valor sigue siendo el que había, y eso es a propósito: cambiar qué se mide y cuánto a la vez deja
|
|
100
|
+
// sin saber cuál de los dos movió el resultado. Lo que se sabe hoy es que **64 está por debajo de lo que
|
|
101
|
+
// una empresa real usa**: una instancia medida tiene 93,8 KB de reglas propias, así que el aviso le sale
|
|
102
|
+
// desde el día que instaló. Elegir el número con esa evidencia es una decisión aparte, y la cuenta que la
|
|
103
|
+
// habilita está en el 141: 61,9 KB ≈ 15,9 K tokens, o sea ~3,9 KB por 1K tokens en **cada** agente.
|
|
88
104
|
const HEAVY = 64 * 1024
|
|
89
105
|
|
|
90
106
|
// La línea que declara el peso, para que la digan igual `install` y `check`. Nombra las dos más grandes
|
|
91
107
|
// porque es lo accionable: saber que el bloque pesa no dice cuál conviene declarar por superficie.
|
|
92
108
|
function weightLine(root) {
|
|
93
|
-
const { count, bytes, files } = weight(root)
|
|
109
|
+
const { count, bytes, own, files } = weight(root)
|
|
94
110
|
const top = files.slice(0, 2).map((one) => path.basename(one.file)).join(', ')
|
|
111
|
+
// El total es lo que paga el agente y lo propio es lo único sobre lo que el proyecto puede hacer algo,
|
|
112
|
+
// así que van los dos: con uno solo, o el número no es el costo real o no es accionable.
|
|
95
113
|
return `el bloque de reglas carga ${count} archivo(s), ${KB(bytes)} en cada agente`
|
|
96
|
-
+ (top ? ` (las más grandes: ${top})` : ''
|
|
114
|
+
+ ` (${KB(own)} propias)${top ? ` (las más grandes: ${top})` : ''}`
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
// Sólo cuando pasó el umbral. Devuelve lista porque es lo que `check` empalma con el resto de avisos.
|
|
100
118
|
function heavyRules(root) {
|
|
101
|
-
return weight(root).
|
|
119
|
+
return weight(root).own > HEAVY ? [weightLine(root)] : []
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
// Sin raíz el marcador queda como está —así lo leen las pruebas que revisan el texto de un adaptador—: un
|
package/engine/cli/contract.js
CHANGED
|
@@ -72,22 +72,38 @@ const MARKED = /^###\s+Límites\s*$/m
|
|
|
72
72
|
// No filtra comentarios y no hace falta: una viñeta comentada arranca con `<!--`, así que el filtro de
|
|
73
73
|
// viñetas ya la descarta. Sacar `withoutComments` de acá fue el resultado de una mutación que sobrevivió
|
|
74
74
|
// —apagarlo no ponía nada en rojo—, que es como se ve una defensa que no defiende de nada.
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// Un solo recorrido de los bloques: las viñetas, que son los límites declarados, y la prosa que las
|
|
76
|
+
// presenta. Las dos salen de acá porque dónde empieza y dónde termina un bloque se decide una vez; con
|
|
77
|
+
// dos recorridos, el que delimita para `limits` y el que delimita para `warnings` se despegan y nada
|
|
78
|
+
// falla.
|
|
79
|
+
//
|
|
80
|
+
// El intro se corta en la primera viñeta y no al final del bloque, y eso es lo único que separa este
|
|
81
|
+
// arreglo de un silenciador. A un bloque no lo cierra nada más que el próximo encabezado, así que el del
|
|
82
|
+
// molde se extiende hasta donde alguien escriba el suyo: medido sobre un banco, el primer bloque se
|
|
83
|
+
// llevaba adentro los cuatro párrafos que la persona había agregado al final de la sección. Descontar el
|
|
84
|
+
// bloque entero —que es lo que parecía el arreglo— apagaba justo lo que el aviso existe para encontrar.
|
|
85
|
+
const BULLET = /^\s*[-*]\s+/
|
|
86
|
+
function marked(raw) {
|
|
87
|
+
const bullets = []
|
|
88
|
+
const intros = []
|
|
77
89
|
let rest = raw
|
|
78
90
|
for (let start = rest.search(MARKED); start >= 0; start = rest.search(MARKED)) {
|
|
79
91
|
const after = rest.slice(start).split('\n').slice(1)
|
|
80
92
|
const end = after.findIndex((line) => /^#{1,3}\s/.test(line))
|
|
81
93
|
const block = end < 0 ? after : after.slice(0, end)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const first = block.findIndex((line) => BULLET.test(line))
|
|
95
|
+
if (first > 0) intros.push(...block.slice(0, first).map((line) => line.trim()).filter(Boolean))
|
|
96
|
+
bullets.push(...block
|
|
97
|
+
.filter((line) => BULLET.test(line))
|
|
98
|
+
.map((line) => line.replace(BULLET, '').trim())
|
|
85
99
|
.filter(Boolean))
|
|
86
100
|
rest = (end < 0 ? '' : after.slice(end).join('\n'))
|
|
87
101
|
}
|
|
88
|
-
return
|
|
102
|
+
return { bullets, intros }
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
const declared = (raw) => marked(raw).bullets
|
|
106
|
+
|
|
91
107
|
function limits(text) {
|
|
92
108
|
const prose = text.split(/\n\s*\n/)
|
|
93
109
|
.map((block) => block.split('\n')
|
|
@@ -128,12 +144,23 @@ function warnings(root) {
|
|
|
128
144
|
const fromTemplate = new Set(paragraphs(
|
|
129
145
|
P.withoutComments(P.section(readIfAny(TEMPLATE_WORKSPACE), /Excepciones de autonom/)),
|
|
130
146
|
))
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
// Se descuenta por línea y no por párrafo, que es donde estaba el defecto: `paragraphs` saca el `- ` y
|
|
148
|
+
// une las viñetas seguidas en un párrafo solo, así que una lista declarada no era igual a ninguna
|
|
149
|
+
// entrada de `declared()` y se contaba entera como un límite perdido (caso 159).
|
|
150
|
+
const { bullets, intros } = marked(mine)
|
|
151
|
+
const suyo = new Set([...bullets, ...intros])
|
|
152
|
+
const outside = P.withoutComments(mine).split('\n')
|
|
153
|
+
.filter((line) => !suyo.has(line.replace(BULLET, '').trim()))
|
|
154
|
+
.join('\n')
|
|
155
|
+
const lost = paragraphs(outside)
|
|
156
|
+
.filter((one) => !fromTemplate.has(one) && !ENUNCIA.test(one))
|
|
134
157
|
if (!lost.length) return []
|
|
158
|
+
// El aviso nombra el camino declarado y no la gramática, aunque los dos sigan valiendo: desde 0.92.0
|
|
159
|
+
// hay una forma de arreglar esto que no pide imitar nada, y mandar a la otra es mandar al camino que
|
|
160
|
+
// el 157 existe para no tener que usar. Quien ya escribió «El runner…» no necesita el aviso — no le
|
|
161
|
+
// sale.
|
|
135
162
|
return [`organization/workspace.md: ${lost.length} párrafo(s) de "## Excepciones de autonomía" no llegan `
|
|
136
|
-
+ 'a los agentes
|
|
163
|
+
+ 'a los agentes. El que sea un límite va como viñeta bajo `### Límites`: '
|
|
137
164
|
+ `${lost.map((one) => `"${one.slice(0, 60)}…"`).join(', ')}`]
|
|
138
165
|
}
|
|
139
166
|
|
package/engine/hooks/run.js
CHANGED
|
@@ -11,6 +11,7 @@ const os = require('node:os')
|
|
|
11
11
|
const path = require('node:path')
|
|
12
12
|
const { readInput, cwdOf, block, findOpsRoot } = require('./input')
|
|
13
13
|
const shell = require('./shell')
|
|
14
|
+
const { verify } = require('./verify')
|
|
14
15
|
const files = require('./files')
|
|
15
16
|
const chat = require('./chat')
|
|
16
17
|
const { secretsShell } = require('./secrets-shell')
|
|
@@ -40,7 +41,7 @@ const guards = {
|
|
|
40
41
|
'git-add': shell.gitAdd,
|
|
41
42
|
dependencies: shell.dependencies,
|
|
42
43
|
governance: shell.governance,
|
|
43
|
-
verify
|
|
44
|
+
verify,
|
|
44
45
|
'shell-boundary': shell.shellBoundary,
|
|
45
46
|
'secrets-shell': secretsShell,
|
|
46
47
|
'ops-config-shell': opsConfigShell,
|