@ingeniomaps/cauce 0.20.0 → 0.22.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.
@@ -111,16 +111,26 @@ if (!contexto || !contexto.items || !contexto.items.length) {
111
111
  // Dónde trabaja el cargo mientras responde. En el toolkit no puede ser acá: no hay `planning/` que
112
112
  // valga, así que se le arma un banco desechable. En una empresa es su propia instancia, y el cargo
113
113
  // tiene que ser suyo —propio o adoptado—: uno del catálogo se evalúa arriba, no acá.
114
- let WORK = ROOT
114
+ // Un banco por caso, no uno por cargo. Con uno compartido los casos corren a la vez sobre el mismo
115
+ // `planning/` y se leen entre sí: uno tomó por «una sesión anterior de este mismo cargo» lo que otro
116
+ // acababa de escribir, y otro evaluó cuatro candidatas que en su enunciado no existían.
117
+ //
118
+ // Un solo agente los prepara —son comandos deterministas— y después la ruta de cada caso se arma
119
+ // sola, sin volver a preguntar.
120
+ const BENCH_ROOT = `${ROOT}/.cauce-eval/${AGENT}`
121
+ let porCaso = null
115
122
  if (contexto.mode === 'toolkit') {
116
123
  const banco = await agent(
117
- `From ${ROOT}, run "node tools/ops.js evaluate ${AGENT} --bench" and report the absolute path it ` +
118
- `printed, nothing else. It recreates a disposable instance where writing to planning/ is legitimate.`,
119
- { schema: BENCH, label: 'banco' },
124
+ `From ${ROOT}, run one command per case, in order, and report only whether all of them printed a ` +
125
+ `path:\n` +
126
+ contexto.items.map((item) => ` node tools/ops.js evaluate ${AGENT} --bench ${item.id}`).join('\n') +
127
+ `\n\nEach one recreates a disposable instance where writing to planning/ is legitimate. Set path ` +
128
+ `to the directory they share: ${BENCH_ROOT}`,
129
+ { schema: BENCH, label: 'bancos' },
120
130
  )
121
- if (!banco || !banco.path) return stop('sin-banco', 'no se pudo preparar el banco de evaluación')
122
- WORK = banco.path
123
- log(`Banco: ${WORK}`)
131
+ if (!banco || !banco.path) return stop('sin-banco', 'no se pudieron preparar los bancos de evaluación')
132
+ porCaso = (item) => `${BENCH_ROOT}/${item.id}`
133
+ log(`Bancos: ${BENCH_ROOT}/<caso>`)
124
134
  } else if (contexto.system) {
125
135
  return stop('cargo-del-catalogo',
126
136
  `${AGENT} lo mantiene Cauce, no esta empresa: evaluarlo acá mediría tu configuración y el ` +
@@ -134,10 +144,11 @@ const veredictos = await pipeline(
134
144
 
135
145
  // Responde el cargo. Recibe su contrato y el pedido; nunca los comportamientos esperados.
136
146
  (item) => agent(
137
- `Trabajás en ${WORK}: esa es tu instancia, con su planning/, su organization/ y su AGENTS.md. ` +
138
- `Todo lo que escribas va ahí.\n\n` +
147
+ `Trabajás en ${porCaso ? porCaso(item) : ROOT}: esa es tu instancia, con su planning/, su ` +
148
+ `organization/ y su AGENTS.md. Todo lo que escribas va ahí.\n\n` +
139
149
  `Actuá como el cargo ${AGENT}, respetando el contrato de ${contexto.skill}: cuándo actuar, qué ` +
140
- `decide, qué no le corresponde y cuál es su entrega mínima. Leé también ${WORK}/AGENTS.md: son las ` +
150
+ `decide, qué no le corresponde y cuál es su entrega mínima. Leé también el AGENTS.md de esa ` +
151
+ `instancia: son las ` +
141
152
  `reglas que todo cargo obedece, y un cargo corre siempre con las dos cosas —medirlo sólo contra su ` +
142
153
  `SKILL.md lo evaluaba en una situación que nunca ocurre—. No leas ningún archivo bajo ` +
143
154
  `evaluations/: no te corresponde y contaminaría la respuesta.\n\n` +
@@ -150,8 +161,18 @@ const veredictos = await pipeline(
150
161
  // mide contra lo que el caso declara, no contra una relectura del SKILL.md.
151
162
  (answer, item) => (answer ? agent(
152
163
  `Un cargo recibió este pedido:\n\n${item.request}\n\nY respondió:\n\n${answer.response}\n\n` +
164
+ (porCaso
165
+ ? `La respuesta no es toda la entrega. El cargo trabajó en ${porCaso(item)}, un banco versionado ` +
166
+ `desde su estado limpio: corré "git -C ${porCaso(item)} status --porcelain" y ` +
167
+ `"git -C ${porCaso(item)} diff" para ver exactamente qué produjo, y leé los archivos que ` +
168
+ `aparezcan. Juzgá la respuesta **y** lo que escribió.\n\n` +
169
+ `Esto no es un detalle: un cargo contestó un resumen y dejó el contrato completo —con firma, ` +
170
+ `orden de verificación y catorce pruebas— en su INBOX. Juzgado sólo por el texto, se lo dio ` +
171
+ `por ausente.\n\n`
172
+ : '') +
153
173
  `Contrastá esa respuesta contra cada comportamiento esperado y decidí si se observa o no. ` +
154
- `Citá la parte de la respuesta que lo sostiene; si no hay cita, no se observa. No premies la ` +
174
+ `Citá la parte de la respuesta —o del archivo que el cargo escribió— que lo sostiene; si no hay ` +
175
+ `cita, no se observa. No premies la ` +
155
176
  `intención ni el tono: sólo lo que la respuesta dice.\n\n` +
156
177
  `Comportamientos esperados:\n${item.expected.map((one, index) => `${index + 1}. ${one}`).join('\n')}\n\n` +
157
178
  `El caso pasa sólo si se observan todos.`,
@@ -5,8 +5,10 @@
5
5
  // 1. Se niega si «Aprobación humana» no está firmada con responsable. Un agente no se autoriza a sí
6
6
  // mismo, y `automatic_apply: false` sigue siendo cierto: esto no corre solo, lo corre alguien
7
7
  // después de firmar.
8
- // 2. Corre los casos adversariales al terminar. Aplicar sin verificar deja un contrato cambiado y
9
- // nadie sabiendo si todavía se sostiene, que es peor que no haber aplicado.
8
+ // 2. Invalida el registro de evaluación anterior y manda a rehacerlo. Aplicar deja un contrato
9
+ // cambiado y nadie sabiendo si todavía se sostiene: el veredicto que había medía la versión
10
+ // vieja. Correr los casos acá no sirve —son un recorrido propio, con su banco por caso y su
11
+ // juez—, así que este termina nombrando `/agent-eval` en vez de fingir que ya verificó.
10
12
  //
11
13
  // Aplica **prosa**, no un parche, y es a propósito: la propuesta dice «agregar dos viñetas después de
12
14
  // la última existente». Un parche envejece si alguien toca el archivo mientras la propuesta espera
@@ -20,7 +22,6 @@ export const meta = {
20
22
  { title: 'Firma', detail: 'Sin aprobación humana no se toca nada' },
21
23
  { title: 'Aplicar', detail: 'El cambio, archivo por archivo' },
22
24
  { title: 'Registrar', detail: 'Historial del cargo' },
23
- { title: 'Verificar', detail: 'Los casos adversariales contra el contrato nuevo' },
24
25
  ],
25
26
  }
26
27
 
@@ -126,8 +127,6 @@ await agent(
126
127
  { label: 'historial' },
127
128
  )
128
129
 
129
- phase('Verificar')
130
-
131
130
  log('El contrato cambió: los casos valen sólo si se vuelven a correr contra la versión nueva.')
132
131
  return finish({
133
132
  agent: AGENT,
@@ -3,10 +3,10 @@ export const meta = {
3
3
  name: 'integration-promote',
4
4
  description: 'Promueve un draft revisado desde staging hacia el roadmap local.',
5
5
  whenToUse: 'Después de que una persona marque un candidato de integración como ready.',
6
+ // Una sola: el recorrido es un agente que encadena check → promote → check. Declarar «Promote» y
7
+ // «Validate» aparte pintaba dos grupos vacíos en el progreso, porque nunca se entraba a ellos.
6
8
  phases: [
7
- { title: 'Preflight', detail: 'Validar proveedor, clave y estado ready.' },
8
- { title: 'Promote', detail: 'Materializar el draft mediante el adaptador común.' },
9
- { title: 'Validate', detail: 'Comprobar planning e integración.' },
9
+ { title: 'Preflight', detail: 'Validar el candidato, promoverlo y comprobar planning.' },
10
10
  ],
11
11
  }
12
12
 
@@ -3,10 +3,10 @@ export const meta = {
3
3
  name: 'integration-sync',
4
4
  description: 'Valida, sincroniza y comprueba staging preservando la curación local.',
5
5
  whenToUse: 'Refrescar trabajo remoto antes de revisarlo y promoverlo a planning.',
6
+ // Una sola: el recorrido es un agente que encadena check → sync → check. Declarar «Sync» y
7
+ // «Validate» aparte pintaba dos grupos vacíos en el progreso, porque nunca se entraba a ellos.
6
8
  phases: [
7
- { title: 'Preflight', detail: 'Resolver proveedor y validar configuración.' },
8
- { title: 'Sync', detail: 'Leer remoto mediante su adaptador y refrescar staging.' },
9
- { title: 'Validate', detail: 'Validar integración y planning sin promover.' },
9
+ { title: 'Preflight', detail: 'Resolver proveedor, sincronizar y revalidar staging.' },
10
10
  ],
11
11
  }
12
12
 
@@ -37,17 +37,9 @@ function inherited(relative) {
37
37
 
38
38
  const TEXT = /\.(md|ya?ml|json|txt)$/i
39
39
 
40
- function tree(dir, prefix = '') {
41
- const found = []
42
- let list = []
43
- try { list = fs.readdirSync(dir, { withFileTypes: true }) } catch { return found }
44
- for (const entry of list) {
45
- const relative = prefix ? `${prefix}/${entry.name}` : entry.name
46
- if (entry.isDirectory()) found.push(...tree(path.join(dir, entry.name), relative))
47
- else found.push(relative)
48
- }
49
- return found.sort()
50
- }
40
+ // El mismo recorrido que usa `upgrade` para comparar instancia contra paquete: un cargo es un árbol
41
+ // de archivos como cualquier otro, y tener dos implementaciones sólo daba dos formas de ordenarlos.
42
+ const tree = ownership.treeFiles
51
43
 
52
44
  // La misma resolución que usa todo lo demás —paquete primero, toolkit después— en vez de deducirla
53
45
  // del directorio del motor: la versión que interesa es la del catálogo del que sale la copia.
@@ -89,7 +81,8 @@ function fork(root, slug, date) {
89
81
  const target = path.join(root, 'agents', type, slug)
90
82
  if (fs.existsSync(target)) throw new Error(`${path.relative(root, target)} ya existe`)
91
83
 
92
- const files = tree(found.dir).filter(inherited)
84
+ const todo = tree(found.dir)
85
+ const files = todo.filter(inherited)
93
86
  if (!files.includes('SKILL.md')) throw new Error(`${slug} no tiene SKILL.md: no hay contrato que copiar`)
94
87
 
95
88
  // El digest sale del origen, no de la copia. Es lo que el catálogo tenía al momento del fork, y es
@@ -118,7 +111,7 @@ function fork(root, slug, date) {
118
111
  forks[slug] = { type, version, files: digests }
119
112
  manifest.write(root, null, null, forks)
120
113
 
121
- return { slug, type, dir: target, files, version, skipped: tree(found.dir).filter((one) => !inherited(one)) }
114
+ return { slug, type, dir: target, files, version, skipped: todo.filter((one) => !inherited(one)) }
122
115
  }
123
116
 
124
117
  // Qué cambió en el catálogo desde que la empresa se llevó su copia.
@@ -15,10 +15,6 @@ const REQUIRED_SECTIONS = [
15
15
  'Aprobación humana',
16
16
  ]
17
17
 
18
- function agentRoot(root, agent) {
19
- return catalog.resolve(root, agent)
20
- }
21
-
22
18
  // Un cargo del sistema vive dentro del paquete: escribir ahí perdería el informe en el próximo
23
19
  // `npm ci`, y además duplicaría en cada empresa una investigación sobre la profesión que se hace
24
20
  // mejor una sola vez. Lo que sí es de esta empresa es su contexto, y ese tiene otro lugar.
@@ -136,7 +132,7 @@ Pendiente.
136
132
  }
137
133
 
138
134
  function evaluate(root, agent) {
139
- const target = agentRoot(root, agent)
135
+ const target = catalog.resolve(root, agent)
140
136
  const errors = []
141
137
  const requiredFiles = [
142
138
  'learning/sources.yaml',
@@ -146,7 +142,7 @@ function evaluate(root, agent) {
146
142
  // `AUTOMATION.md` documenta cómo corre la automatización de aprendizaje del toolkit. Exigírselo
147
143
  // a una empresa que escribe un cargo propio era pedirle contabilidad interna nuestra: su cargo debe
148
144
  // tener contrato, fuentes e historia, no nuestro andamiaje.
149
- if (require('./catalog').find(root, agent).system) requiredFiles.push('learning/AUTOMATION.md')
145
+ if (catalog.find(root, agent).system) requiredFiles.push('learning/AUTOMATION.md')
150
146
  for (const relative of requiredFiles) {
151
147
  if (!fs.existsSync(path.join(target, relative))) errors.push(`falta ${relative}`)
152
148
  }
@@ -12,6 +12,11 @@ const M = require('../core/manifest')
12
12
 
13
13
  const RUNNER_NAMES = ['claude', 'codex', 'gemini', 'antigravity']
14
14
 
15
+ // Los guards que el motor implementa, tomados del registro que los ejecuta. Sale de ahí y no de un
16
+ // número escrito a mano: `automation check` anunciaba «11 guards» cuando hacía rato que eran doce, y
17
+ // un conteo que envejece solo es peor que ninguno —dice que revisó menos de lo que revisó—.
18
+ const GUARD_NAMES = Object.keys(H.guards)
19
+
15
20
  // Adaptadores y workflows viven en el paquete, no en la instancia: son definiciones que el motor
16
21
  // consume y que ninguna empresa edita —`RUNNER_NAMES` es cerrado, así que ni siquiera puede agregar
17
22
  // uno propio—. Los hooks sí se quedan en el proyecto: la configuración del runner los nombra por
@@ -172,7 +177,7 @@ Respetá los límites de ese contrato y las reglas de \`AGENTS.md\`. Generado po
172
177
  }
173
178
 
174
179
  function installRoleSkills(root, runner, output) {
175
- if (!runner.capabilities.nativeSkills || !runner.roleSkills) return 0
180
+ if (!runner.capabilities.nativeSkills || !runner.roleSkills) return
176
181
  const install = installRoot(root)
177
182
  const base = F.assertWithin(install, path.resolve(install, runner.roleSkills), `${runner.name}: roleSkills`)
178
183
  const roles = roleCatalog(root)
@@ -182,7 +187,6 @@ function installRoleSkills(root, runner, output) {
182
187
  F.atomicWrite(file, roleSkill(role))
183
188
  }
184
189
  if (roles.length) output.log(`✓ ${runner.name}: ${roles.length} cargo(s) disponibles en ${runner.roleSkills}`)
185
- return roles.length
186
190
  }
187
191
 
188
192
  // Runners que además de los archivos necesitan un registro propio para que el wiring cuente. Copiar
@@ -267,8 +271,12 @@ function check(root) {
267
271
  if (!O.engineAt(root, path.join('hooks', 'run.js'))) {
268
272
  errors.push('falta engine/hooks/run.js: corré "npm install" en la raíz del repo ops')
269
273
  }
270
- const workflows = ['autobuild.js', 'team.js', path.join('integrations', 'sync.js')]
271
- workflows.push(path.join('integrations', 'promote.js'))
274
+ const workflows = [
275
+ 'autobuild.js',
276
+ 'team.js',
277
+ path.join('integrations', 'sync.js'),
278
+ path.join('integrations', 'promote.js'),
279
+ ]
272
280
  const packaged = packagedAutomation(root)
273
281
  for (const name of workflows) {
274
282
  if (!packaged || !fs.existsSync(path.join(packaged, 'workflows', name))) {
@@ -550,16 +558,14 @@ function listHooks(output = console) {
550
558
  }
551
559
 
552
560
  module.exports = {
561
+ GUARD_NAMES,
553
562
  RUNNER_NAMES,
554
563
  check,
555
564
  doctor,
556
- includesConfig,
557
565
  install,
558
566
  legacyGuardWiring,
559
- pruneSupersededHooks,
560
567
  roleCatalog,
561
568
  roleSkill,
562
569
  listHooks,
563
- mergeConfig,
564
570
  runnerManifest,
565
571
  }
package/engine/cli/ops.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs')
5
5
  const path = require('path')
6
+ const { spawnSync } = require('child_process')
6
7
  const P = require('../planning/parser')
7
8
  const B = require('../planning/business-rules')
8
9
  const PC = require('../planning/contracts')
@@ -49,7 +50,7 @@ function usage() {
49
50
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
50
51
  ops automation install <ops-root> claude|codex|gemini|antigravity
51
52
  ops learn <agent> [--proposal]
52
- ops evaluate <agent> [--cases [--json]] [--bench]
53
+ ops evaluate <agent> [--cases [--json]] [--bench [caso]]
53
54
  ops agents list [ops-root] [--own|--system] [--json]
54
55
  ops agents fork <cargo> [ops-root]
55
56
  ops team list
@@ -191,12 +192,34 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
191
192
  // es una épica o una entrada de INBOX no tiene dónde escribir, así que se niega. Con razón, y su caso
192
193
  // lo cuenta como fallo: eso midió una configuración, no al cargo.
193
194
  //
195
+ // **Uno por caso**, y esto se aprendió corriendo. Con un banco compartido, los cinco casos de un
196
+ // cargo corren a la vez sobre el mismo `planning/` y se leen entre sí: un caso tomó por «una sesión
197
+ // anterior de este mismo cargo» lo que otro acababa de escribir, y otro evaluó cuatro candidatas que
198
+ // en su enunciado no existían. Ninguno de los dos cambió de veredicto, pero las respuestas ya no eran
199
+ // las que el caso pedía medir. La independencia entre casos es la premisa de medir con ellos.
200
+ //
194
201
  // Se recrea entero en cada corrida. Reutilizarlo dejaría que lo que un cargo escribió el lunes sea
195
202
  // contexto del que responde el martes, y dos corridas del mismo caso dejarían de ser comparables.
196
203
  // Queda en disco al terminar, gitignorado, porque después de un veredicto raro lo primero que uno
197
204
  // quiere es mirar qué escribió el cargo.
198
- function evaluationBench(root) {
199
- const dir = path.join(root, '.cauce-eval')
205
+ function evaluationBench(root, agent, caso) {
206
+ const safe = (value) => {
207
+ if (!/^[a-z0-9_][a-z0-9._-]*$/i.test(value) || value.includes('..')) {
208
+ fail(`nombre inválido para el banco: ${value}`, 2)
209
+ }
210
+ return value
211
+ }
212
+ const dir = path.join(root, '.cauce-eval', safe(agent), safe(caso || '_libre'))
213
+ // Recrear un banco donde alguien ya trabajó borra la evidencia de esa corrida, y el registro de la
214
+ // evaluación se escribe **desde** el banco. Pasó de verdad: se rehizo un banco para probar otra cosa
215
+ // y con él se fue lo que el cargo había escrito; el juez leyó un directorio vacío y concluyó que la
216
+ // respuesta afirmaba algo inexistente. Con el banco versionado, «acá se trabajó» es una pregunta que
217
+ // git contesta exacto.
218
+ const sucio = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' })
219
+ if ((sucio.stdout || '').trim() && !process.argv.includes('--force')) {
220
+ fail(`${dir} tiene trabajo sin recoger. Guardá el registro de esa corrida antes de rehacerlo, `
221
+ + 'o usá --force si ya lo tenés.', 2)
222
+ }
200
223
  fs.rmSync(dir, { recursive: true, force: true })
201
224
  scaffold(dir, { name: 'Banco de evaluación', mode: 'sidecar', quiet: true })
202
225
  // El motor por symlink: la misma resolución que en una instancia real —`node_modules/@ingeniomaps`—
@@ -204,6 +227,22 @@ function evaluationBench(root) {
204
227
  const scope = path.join(dir, 'node_modules', '@ingeniomaps')
205
228
  fs.mkdirSync(scope, { recursive: true })
206
229
  fs.symlinkSync(PROJECT_ROOT, path.join(scope, 'cauce'), 'dir')
230
+
231
+ // El banco queda versionado desde su estado limpio, y eso resuelve un problema de medición: la
232
+ // entrega de un cargo puede no estar en su respuesta. `backend-engineer` contestó un resumen del
233
+ // webhook y escribió el contrato —firma, orden de verificación, ventana antirreplay, catorce
234
+ // pruebas— en su `INBOX.md`. El juez, que sólo leía la respuesta, lo dio por ausente y lo reprobó.
235
+ //
236
+ // Con el banco versionado, `git status` y `git diff` muestran exactamente qué produjo, separado del
237
+ // andamiaje. Es la diferencia entre juzgar el resumen y juzgar la entrega. Se ignora `node_modules`
238
+ // porque es un symlink al toolkit y no es obra del cargo.
239
+ const git = (...args) => spawnSync('git', ['-C', dir, ...args], { stdio: 'ignore' })
240
+ fs.appendFileSync(path.join(dir, '.gitignore'), '\nnode_modules/\n')
241
+ git('init', '-q')
242
+ git('config', 'user.email', 'banco@cauce.local')
243
+ git('config', 'user.name', 'banco de evaluación')
244
+ git('add', '-A')
245
+ git('commit', '-q', '-m', 'banco limpio')
207
246
  return dir
208
247
  }
209
248
 
@@ -513,13 +552,6 @@ function instanceVersion(root) {
513
552
  } catch { return '' }
514
553
  }
515
554
 
516
- // Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
517
- // o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
518
- function overlayTree(from, to, root, skip = []) {
519
- F.assertNoSymlinkPath(root, to)
520
- copyRuntime(from, to, false, root, skip)
521
- }
522
-
523
555
  // Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
524
556
  // propias, agentes editados— queda intacto por construcción, no por comparación.
525
557
  function upgrade(dir) {
@@ -581,7 +613,9 @@ function upgrade(dir) {
581
613
  const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
582
614
  if (!fs.existsSync(origin)) continue
583
615
  const target = path.join(root, relative)
584
- if (fs.statSync(origin).isDirectory()) overlayTree(origin, target, root)
616
+ // Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
617
+ // o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
618
+ if (fs.statSync(origin).isDirectory()) copyRuntime(origin, target, false, root)
585
619
  else {
586
620
  F.assertNoSymlinkPath(root, target)
587
621
  F.atomicWrite(target, fs.readFileSync(origin, 'utf8'))
@@ -650,8 +684,6 @@ function upgrade(dir) {
650
684
  for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
651
685
  }
652
686
 
653
- // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
654
- // reimplemente el recorrido del catálogo.
655
687
  // La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
656
688
  // vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
657
689
  function opsRoot(dir) {
@@ -672,6 +704,8 @@ function agentsFork(slug, dir) {
672
704
  console.log(` reinstalá tu runner para que ${slug} apunte a tu copia`)
673
705
  }
674
706
 
707
+ // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
708
+ // reimplemente el recorrido del catálogo.
675
709
  function agents(action, dir, extra) {
676
710
  if (action === 'fork') return agentsFork(dir, extra)
677
711
  if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
@@ -845,7 +879,9 @@ function automation(action, rootArg, runnerName) {
845
879
  const errors = A.check(root)
846
880
  for (const error of errors) console.error(`✗ ${error}`)
847
881
  if (errors.length) fail(`${errors.length} error(es) de automatización`)
848
- console.log(`✓ automatización válida: 11 guards, ${A.RUNNER_NAMES.length} adaptadores`)
882
+ console.log(
883
+ `✓ automatización válida: ${A.GUARD_NAMES.length} guards, ${A.RUNNER_NAMES.length} adaptadores`,
884
+ )
849
885
  return
850
886
  }
851
887
  if (action === 'doctor') {
@@ -895,7 +931,11 @@ function evaluate(agent) {
895
931
  fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: si es del '
896
932
  + `catálogo, adoptalo primero con "ops agents fork ${agent}".`, 2)
897
933
  }
898
- return console.log(evaluationBench(root))
934
+ // El caso viene detrás de la bandera: `evaluate <cargo> --bench <caso>`. Sin él se arma un banco
935
+ // suelto, para mirarlo a mano; una corrida real pide uno por caso.
936
+ const flag = process.argv.indexOf('--bench')
937
+ const caso = (process.argv[flag + 1] || '').startsWith('--') ? '' : process.argv[flag + 1]
938
+ return console.log(evaluationBench(root, agent, caso))
899
939
  }
900
940
  try {
901
941
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
@@ -51,5 +51,4 @@ module.exports = {
51
51
  assertWithin,
52
52
  atomicWrite,
53
53
  atomicWriteJson,
54
- isWithin,
55
54
  }
@@ -94,5 +94,5 @@ function edited(root, relative, files) {
94
94
  }
95
95
 
96
96
  module.exports = {
97
- FILE, digest, digestText, edited, prune, read, readForks, readRunners, record, write,
97
+ digest, digestText, edited, prune, read, readForks, readRunners, record, write,
98
98
  }
@@ -80,28 +80,13 @@ function sourceOf(relative) {
80
80
  return relative
81
81
  }
82
82
 
83
- // Dónde puede estar el motor. Lo mismo que resuelven `tools/ops.js`, el wrapper de hooks y el bridge
84
- // de Antigravity: declararlo una vez evita que un consumidor quede afuera.
83
+ // Una ruta cualquiera del paquete: primero la dependencia npm, después el propio repositorio del
84
+ // toolkit corriendo sobre mismo. La usan los adaptadores de runner, los workflows y el motor
85
+ // mismo: el motor los consume, el proyecto no los materializa.
85
86
  //
86
- // Dos caminos, no tres: el motor llega por npm, y la tercera entrada es el propio repositorio del
87
- // toolkit corriendo sobre sí mismo. La copia vendorizada en `.ops/` se retiró en 0.10.0 — ahorraba un
87
+ // Dos caminos, no tres. La copia vendorizada en `.ops/` se retiró en 0.10.0 ahorraba un
88
88
  // `package.json` a cambio de 5 MB en la historia de la empresa y de no poder enterarse de una versión
89
89
  // nueva, y Node hace falta igual en los dos casos.
90
- function engineCandidates(root) {
91
- return [
92
- path.join(root, 'node_modules', '@ingeniomaps', 'cauce', 'engine'),
93
- path.join(root, 'engine'),
94
- ]
95
- }
96
-
97
- function engineAt(root, relative = '') {
98
- return engineCandidates(root)
99
- .map((dir) => (relative ? path.join(dir, relative) : dir))
100
- .find((candidate) => fs.existsSync(candidate)) || ''
101
- }
102
-
103
- // Una ruta cualquiera del paquete, en el mismo orden de preferencia que el motor. La usan los
104
- // adaptadores de runner y los workflows: el motor los consume, el proyecto no los materializa.
105
90
  function packagePath(root, relative) {
106
91
  const candidates = [
107
92
  path.join(root, 'node_modules', '@ingeniomaps', 'cauce', relative),
@@ -110,6 +95,14 @@ function packagePath(root, relative) {
110
95
  return candidates.find((candidate) => fs.existsSync(candidate)) || ''
111
96
  }
112
97
 
98
+ // Dónde quedó el motor. Es `packagePath` bajo `engine/`, y no una cascada propia: había dos copias de
99
+ // la misma lista de candidatos en este archivo, una al lado de la otra, y nada obligaba a que
100
+ // siguieran diciendo lo mismo. La tercera copia vive en el bridge de Antigravity, que corre antes de
101
+ // poder cargar este módulo y por eso la repite a propósito.
102
+ function engineAt(root, relative = '') {
103
+ return packagePath(root, relative ? path.join('engine', relative) : 'engine')
104
+ }
105
+
113
106
  // Definiciones que consume el motor —cargos y equipos— y que por eso viajan con el paquete en vez
114
107
  // de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
115
108
  // proyecto deba crear. Una sola implementación para las dos, o divergen.
@@ -165,6 +158,10 @@ const RETIRED = [
165
158
  '.github/workflows/agent-learning.yml',
166
159
  'automatization/runners',
167
160
  'automatization/workflows',
161
+ // Prometía ser el interruptor de los guards —«runner activo y gates requeridos», decía su README—
162
+ // y no lo leía nadie. Quien corría `install claude` seguía viendo `"runner": "manual"`, y un cargo
163
+ // que lo leyó dio por configurados unos gates que en realidad decide la configuración del runner.
164
+ 'automatization/config.json',
168
165
  ]
169
166
 
170
167
  // Aprendizaje que quedó dentro de una ruta retirada. Es lo único ahí que no se puede reponer, así
@@ -234,11 +231,9 @@ module.exports = {
234
231
  RUNTIME_PATHS,
235
232
  retiredWithLearning,
236
233
  engineAt,
237
- engineCandidates,
238
234
  packagePath,
239
235
  SYSTEM_COLLECTIONS,
240
236
  SYSTEM_FILES,
241
- identity,
242
237
  localChanges,
243
238
  overrides,
244
239
  sourceOf,
@@ -459,5 +459,5 @@ if (require.main === module) {
459
459
  }
460
460
 
461
461
  module.exports = {
462
- commandOf, execute, executeAll, fileOf, filesOf, findOpsRoot, guards, hookGroups, hookMetadata,
462
+ execute, executeAll, findOpsRoot, guards, hookGroups, hookMetadata,
463
463
  }
@@ -18,10 +18,6 @@ function readJson(file) {
18
18
  }
19
19
  }
20
20
 
21
- function writeJson(file, value) {
22
- F.atomicWriteJson(file, value)
23
- }
24
-
25
21
  function safeSegment(value, label) {
26
22
  const segment = String(value || '')
27
23
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment) || segment === '.' || segment === '..') {
@@ -95,7 +91,7 @@ function validate(root, onlyProvider = '') {
95
91
  const secret = sensitivePath(loaded.config)
96
92
  if (secret) errors.push(`${name}: ${secret} no puede contener secretos; usa una variable de entorno`)
97
93
  try {
98
- adapter(name).validateConfig(loaded.config, errors, warnings)
94
+ adapter(name).validateConfig(loaded.config, errors)
99
95
  } catch (error) {
100
96
  errors.push(`${name}: ${error.message}`)
101
97
  }
@@ -254,11 +250,11 @@ async function sync(root, name, options = {}) {
254
250
  }
255
251
  fs.mkdirSync(desiredDir, { recursive: true })
256
252
  F.atomicWrite(draftFile, draft)
257
- writeJson(snapshotFile, snapshot)
253
+ F.atomicWriteJson(snapshotFile, snapshot)
258
254
  }
259
255
  const complete = options.complete !== false
260
256
  if (complete) cleanupMissing(root, name, seen, result)
261
- writeJson(path.join(staging, 'sync-state.json'), {
257
+ F.atomicWriteJson(path.join(staging, 'sync-state.json'), {
262
258
  schemaVersion: 2,
263
259
  provider: name,
264
260
  complete,
@@ -284,7 +280,7 @@ function cleanupMissing(root, name, seen, result) {
284
280
  continue
285
281
  }
286
282
  snapshot.sync.missingFromRemote = true
287
- writeJson(snapshotFile, snapshot)
283
+ F.atomicWriteJson(snapshotFile, snapshot)
288
284
  result.missing++
289
285
  }
290
286
  }
@@ -364,7 +360,7 @@ function promote(root, name, key) {
364
360
  draft = replaceField(draft, 'promotedAt', `"${new Date().toISOString()}"`)
365
361
  F.atomicWrite(draftFile, draft)
366
362
  snapshot.sync.draftChanged = true
367
- writeJson(path.join(dir, 'remote.json'), snapshot)
363
+ F.atomicWriteJson(path.join(dir, 'remote.json'), snapshot)
368
364
  return { key, kind: fields.promotionKind }
369
365
  }
370
366
 
@@ -385,13 +381,11 @@ const oneLine = (value) => String(value).replace(/\s+/g, ' ').trim()
385
381
  module.exports = {
386
382
  STATES,
387
383
  adapter,
388
- draftSections,
389
384
  frontmatter,
390
385
  promote,
391
386
  providerConfig,
392
387
  reconcile,
393
388
  safeSegment,
394
- stagingItems,
395
389
  sync,
396
390
  validate,
397
391
  writebackPlan,
@@ -220,7 +220,6 @@ function reconcile(root, provider, operation, keys = []) {
220
220
  }
221
221
 
222
222
  module.exports = {
223
- CRITICAL_FIELDS,
224
223
  STATES,
225
224
  categoryOf,
226
225
  derive,
@@ -79,9 +79,6 @@ function validateRoadmapStructure(dir) {
79
79
  }
80
80
 
81
81
  module.exports = {
82
- DECISION_TRACE,
83
- EPIC_AUXILIARY_FILES,
84
- TEST_TRACE,
85
82
  validDecisionTrace,
86
83
  validTestTrace,
87
84
  validateDoneEntry,
@@ -6,7 +6,6 @@ const fs = require('fs')
6
6
  const path = require('path')
7
7
 
8
8
  const EPIC_STATES = ['open', 'active', 'closed']
9
- const TIERS = ['directo', 'lite', 'full']
10
9
 
11
10
  function read(file) {
12
11
  try { return fs.readFileSync(file, 'utf8') } catch { return '' }
@@ -62,7 +61,7 @@ function criteriaRefs(text) {
62
61
  return [...new Set(refs)]
63
62
  }
64
63
 
65
- function readEpics(dir, { includeTemplates = false } = {}) {
64
+ function readEpics(dir) {
66
65
  return epicFiles(dir).map(({ name, file }) => {
67
66
  const text = read(file)
68
67
  const field = frontmatter(text)
@@ -76,7 +75,6 @@ function readEpics(dir, { includeTemplates = false } = {}) {
76
75
  const stories = [...section(text, /Historias/i).matchAll(storyPattern)]
77
76
  .map((match) => ({
78
77
  slug: match[1].trim(),
79
- text: match[2].replace(/\s+/g, ' ').trim(),
80
78
  criteria: criteriaRefs(match[2]),
81
79
  service: ((match[2].match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
82
80
  }))
@@ -86,12 +84,11 @@ function readEpics(dir, { includeTemplates = false } = {}) {
86
84
  num: field('epic'),
87
85
  title: field('title'),
88
86
  status: field('status'),
89
- service: field('service'),
90
87
  criteria,
91
88
  stories,
92
89
  hasContext: /^##\s+Contexto relevante/im.test(text),
93
90
  }
94
- }).filter((epic) => includeTemplates || epic.status !== 'template' && epic.num !== '000')
91
+ }).filter((epic) => epic.status !== 'template' && epic.num !== '000')
95
92
  }
96
93
 
97
94
  function readBacklog(dir) {
@@ -110,7 +107,7 @@ function readBacklog(dir) {
110
107
  if (!task || !current) continue
111
108
  const rest = task[3]
112
109
  current.tasks.push({
113
- slug: task[1].trim(), tier: task[2] || '', text: rest,
110
+ slug: task[1].trim(), tier: task[2] || '',
114
111
  epic: ((rest.match(/\(epic:\s*(\d{3})\)/) || [])[1] || ''),
115
112
  service: ((rest.match(/\(service:\s*([^)]+)\)/) || [])[1] || '').trim(),
116
113
  acceptance: ((rest.match(/_Aceptaci[oó]n:\s*([^_]+)_/i) || [])[1] || '').trim(),
@@ -180,5 +177,5 @@ function readInbox(dir) {
180
177
  }
181
178
 
182
179
  module.exports = {
183
- EPIC_STATES, TIERS, read, frontmatter, readEpics, readBacklog, readDone, readWip, readInbox,
180
+ EPIC_STATES, read, frontmatter, readEpics, readBacklog, readDone, readWip, readInbox,
184
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "type": "commonjs",
6
6
  "bin": {