@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 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, 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
@@ -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 peso deja de ser el costo de arrancar y pasa a ser una decisión que conviene mirar.
84
- // El piso que Cauce impone —las cuatro reglas del sistema— son 38,3 KB, así que un umbral por debajo de
85
- // eso avisaría en toda instancia recién creada y se apagaría por ruido el primer día: eso descartó los
86
- // 60 KB que el caso 141 proponía. 64 KB deja ~26 KB para lo propio, que son varias reglas de tamaño
87
- // normal, antes de que el aviso hable.
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).bytes > HEAVY ? [weightLine(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
@@ -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
- function declared(raw) {
76
- const out = []
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
- out.push(...block
83
- .filter((line) => /^\s*[-*]\s+/.test(line))
84
- .map((line) => line.replace(/^\s*[-*]\s+/, '').trim())
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 out
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
- const declaredHere = new Set(declared(mine))
132
- const lost = paragraphs(P.withoutComments(mine))
133
- .filter((one) => !fromTemplate.has(one) && !ENUNCIA.test(one) && !declaredHere.has(one))
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 porque no arrancan con «El runner», «Debe» o «Nunca»: '
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
 
@@ -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: shell.verify,
44
+ verify,
44
45
  'shell-boundary': shell.shellBoundary,
45
46
  'secrets-shell': secretsShell,
46
47
  'ops-config-shell': opsConfigShell,