@ingeniomaps/cauce 0.17.0 → 0.18.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
@@ -8,6 +8,34 @@ esa operación sea confiable en vez de sólo cómoda: acá se lee qué cambió a
8
8
  un cambio en el protocolo, en las reglas del sistema o en un guard es visible para el usuario y sube
9
9
  minor aunque no toque una sola línea de código.
10
10
 
11
+ ## [0.18.0] - 2026-08-16
12
+
13
+ ### Agregado
14
+
15
+ - **`ops evaluate <cargo> --bench`: un banco desechable donde un cargo del catálogo puede realmente
16
+ trabajar.** El toolkit no es una raíz ops y no puede serlo —el único `planning/` que vive acá es
17
+ `template/planning`, el molde que se distribuye—. Un cargo cuya entrega es una épica o una entrada de
18
+ INBOX no tenía dónde escribir, se negaba con razón, y su caso lo contaba como fallo: el número
19
+ describía el lugar, no al cargo.
20
+
21
+ El banco es una instancia de verdad: `check` pasa, el catálogo resuelve desde adentro y `planning/`
22
+ está vacío y escribible. Se recrea entero en cada corrida —reutilizarlo dejaría que lo que un cargo
23
+ escribió el lunes sea contexto del que responde el martes— y queda en disco al terminar, gitignorado,
24
+ porque después de un veredicto raro lo primero que uno quiere es mirar qué escribió el cargo.
25
+
26
+ ### Cambiado
27
+
28
+ - **La evaluación corre sobre el banco en vez de negarse.** La 0.16.0 detuvo el recorrido dentro del
29
+ toolkit: acertó el diagnóstico y erró el remedio, porque negarse dejó al catálogo sin ninguna forma
30
+ de medirse, y el catálogo es nuestro y nos toca medirlo.
31
+
32
+ El veredicto se escribe junto al cargo, no en el banco: el banco se borra en la corrida siguiente
33
+ —es donde el cargo trabajó, no donde vive— y el veredicto pertenece al contrato que lo rindió.
34
+
35
+ En una empresa no hay banco ni hace falta: su instancia ya es el lugar. Lo que se exige ahí es que el
36
+ cargo sea suyo —propio o adoptado con `agents fork`—, porque evaluar uno del catálogo mediría su
37
+ configuración y dejaría el registro sin dónde vivir.
38
+
11
39
  ## [0.17.0] - 2026-08-16
12
40
 
13
41
  ### Agregado
@@ -7,15 +7,18 @@
7
7
  // Si los viera, el caso mediría su capacidad de repetirlos. Y quien juzga no es quien respondió, por
8
8
  // la misma razón por la que nadie corrige su propio examen.
9
9
  //
10
- // **Dónde se corre importa.** Un cargo cuyo trabajo es producir artefactos de planning una épica, una
11
- // entrada de INBOX— necesita un `planning/` donde escribir sea legítimo. Corrido dentro del repositorio
12
- // del toolkit, ese directorio es `template/planning`, que se distribuye a cada instalación: el cargo se
13
- // niega, con razón, y su caso lo cuenta como fallo. Medido así, `product-manager` falla exactamente los
14
- // dos casos que piden escribir, y ninguno de los otros tres.
10
+ // **Un cargo necesita un lugar donde trabajar.** Su entrega puede ser una épica o una entrada de
11
+ // INBOX, y para eso hace falta un `planning/` donde escribir sea legítimo. El toolkit no lo tiene ni
12
+ // puede tenerlo: el único `planning/` que vive acá es `template/planning`, el molde que se distribuye.
13
+ // Medido así, `product-manager` fallaba exactamente los dos casos que piden escribir y ninguno de los
14
+ // otros tres el número no hablaba del cargo sino del lugar.
15
15
  //
16
- // Por eso el recorrido se niega cuando `mode` es `toolkit`. Dejarlo escrito en este comentario no
17
- // alcanzaba: un comentario no impide nada, y la primera vez que pasó fue justamente porque estaba
18
- // documentado y nadie lo leyó a tiempo. Evaluar los cargos que Cauce distribuye exige una instancia.
16
+ // Por eso en el toolkit se le arma un banco desechable —`evaluate <cargo> --bench`— y el cargo trabaja
17
+ // ahí. El veredicto, en cambio, se escribe junto al cargo: el banco se borra, el contrato queda.
18
+ //
19
+ // En una empresa no hay banco ni hace falta: su instancia ya es el lugar. Lo que se exige ahí es que el
20
+ // cargo sea suyo —propio o adoptado con `agents fork`—, porque evaluar uno del catálogo mediría su
21
+ // configuración y dejaría el registro sin dónde vivir.
19
22
  //
20
23
  // La respuesta no lleva tope de extensión, y eso se probó: con un tope de doce líneas, dos casos que
21
24
  // pasan fallaban. Un comportamiento esperado puede exigir seis elementos —«versión, entorno, datos,
@@ -52,9 +55,15 @@ const CASES = {
52
55
  } },
53
56
  skill: { type: 'string' },
54
57
  mode: { type: 'string' },
58
+ system: { type: 'boolean' },
55
59
  },
56
60
  }
57
61
 
62
+ const BENCH = {
63
+ type: 'object', additionalProperties: false, required: ['path'],
64
+ properties: { path: { type: 'string' } },
65
+ }
66
+
58
67
  const ANSWER = {
59
68
  type: 'object', additionalProperties: false, required: ['response'],
60
69
  properties: { response: { type: 'string' } },
@@ -92,17 +101,31 @@ const contexto = await agent(
92
101
  `2. "node tools/ops.js agents list --json" — set skill to "${ROOT}/<path>/SKILL.md" using the path it ` +
93
102
  `printed for ${AGENT}. That command prints paths relative to ${ROOT} and the next agents run from ` +
94
103
  `elsewhere, so the prefix is not optional.\n` +
95
- `Then read ${ROOT}/ops.config.json and set mode to its "mode" field, verbatim.`,
104
+ `Then read ${ROOT}/ops.config.json and set mode to its "mode" field, verbatim, and set system ` +
105
+ `to what "agents list --json" reported for ${AGENT} in its "system" field.`,
96
106
  { schema: CASES, label: 'cases' },
97
107
  )
98
108
  if (!contexto || !contexto.items || !contexto.items.length) {
99
109
  return stop('sin-casos', `${AGENT} no tiene casos, o no se pudieron leer`)
100
110
  }
111
+ // Dónde trabaja el cargo mientras responde. En el toolkit no puede ser acá: no hay `planning/` que
112
+ // valga, así que se le arma un banco desechable. En una empresa es su propia instancia, y el cargo
113
+ // tiene que ser suyo —propio o adoptado—: uno del catálogo se evalúa arriba, no acá.
114
+ let WORK = ROOT
101
115
  if (contexto.mode === 'toolkit') {
102
- return stop('en-el-toolkit',
103
- 'este recorrido mide cargos trabajando, y acá no pueden: `planningDir` apunta a la plantilla que ' +
104
- 'se distribuye, así que un cargo que deba escribir en planning se niega —con razón— y su caso lo ' +
105
- 'cuenta como fallo. Medido así el resultado no dice nada del cargo. Corrélo desde una instancia.')
116
+ 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' },
120
+ )
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}`)
124
+ } else if (contexto.system) {
125
+ return stop('cargo-del-catalogo',
126
+ `${AGENT} lo mantiene Cauce, no esta empresa: evaluarlo acá mediría tu configuración y el ` +
127
+ `registro no tendría dónde vivir. Si querés una versión tuya, adoptalo con ` +
128
+ `"node tools/ops.js agents fork ${AGENT}" y evaluá esa.`)
106
129
  }
107
130
  log(`${contexto.items.length} caso(s) de ${AGENT}`)
108
131
 
@@ -111,8 +134,10 @@ const veredictos = await pipeline(
111
134
 
112
135
  // Responde el cargo. Recibe su contrato y el pedido; nunca los comportamientos esperados.
113
136
  (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` +
114
139
  `Actuá como el cargo ${AGENT}, respetando el contrato de ${contexto.skill}: cuándo actuar, qué ` +
115
- `decide, qué no le corresponde y cuál es su entrega mínima. Leé también ${ROOT}/AGENTS.md: son las ` +
140
+ `decide, qué no le corresponde y cuál es su entrega mínima. Leé también ${WORK}/AGENTS.md: son las ` +
116
141
  `reglas que todo cargo obedece, y un cargo corre siempre con las dos cosas —medirlo sólo contra su ` +
117
142
  `SKILL.md lo evaluaba en una situación que nunca ocurre—. No leas ningún archivo bajo ` +
118
143
  `evaluations/: no te corresponde y contaminaría la respuesta.\n\n` +
@@ -149,10 +174,11 @@ const filas = hechos.map((one) => {
149
174
  }).join('\n\n')
150
175
 
151
176
  await agent(
152
- `Escribí ${ROOT}/agents/roles/${AGENT}/evaluations/results/<fecha>.md, o la ruta equivalente si el ` +
153
- `cargo vive en el paquete —usá el directorio del cargo que ya conocés por ${contexto.skill}, ` +
154
- `reemplazando SKILL.md por evaluations/results/—. La fecha es la de hoy en formato AAAA-MM-DD; ` +
155
- `obtenela con "date +%F". Creá el directorio si no existe.\n\n` +
177
+ `Escribí el registro junto al cargo: tomá ${contexto.skill} y reemplazá SKILL.md por ` +
178
+ `evaluations/results/<fecha>.md. Creá el directorio si no existe.\n\n` +
179
+ `Ahí y no en el banco de trabajo. El banco se borra en la próxima corrida —es donde el cargo ` +
180
+ `trabajó, no donde vive—, mientras que el veredicto pertenece al contrato que lo rindió y viaja ` +
181
+ `con él. La fecha es la de hoy en formato AAAA-MM-DD; obtenela con "date +%F".\n\n` +
156
182
  `El archivo lleva este frontmatter y después el contenido tal cual te lo paso, sin reescribirlo ni ` +
157
183
  `resumirlo:\n\n---\nagent: ${AGENT}\ndate: <fecha>\npassed: ${pasan.length}\ntotal: ${hechos.length}\n---\n\n` +
158
184
  `# Casos adversariales — <fecha>\n\n${filas}\n\n` +
package/engine/cli/ops.js CHANGED
@@ -49,7 +49,7 @@ function usage() {
49
49
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
50
50
  ops automation install <ops-root> claude|codex|gemini|antigravity
51
51
  ops learn <agent> [--proposal]
52
- ops evaluate <agent> [--cases [--json]]
52
+ ops evaluate <agent> [--cases [--json]] [--bench]
53
53
  ops agents list [ops-root] [--own|--system] [--json]
54
54
  ops agents fork <cargo> [ops-root]
55
55
  ops team list
@@ -79,7 +79,7 @@ function option(name, fallback = '') {
79
79
  return index >= 0 ? process.argv[index + 1] || fallback : fallback
80
80
  }
81
81
 
82
- function copyTemplate(source, target, replacements, force, skip = []) {
82
+ function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
83
83
  F.assertNoSymlinkPath(path.dirname(target), target)
84
84
  fs.mkdirSync(target, { recursive: true })
85
85
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
@@ -88,18 +88,18 @@ function copyTemplate(source, target, replacements, force, skip = []) {
88
88
  // npm no incluye un `.gitignore` dentro de un tarball, así que viaja sin punto y se restituye
89
89
  // acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
90
90
  const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
91
- if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip)
91
+ if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
92
92
  else {
93
93
  if (fs.existsSync(to)) {
94
94
  if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
95
- console.log(`= conservado ${to}`)
95
+ if (!quiet) console.log(`= conservado ${to}`)
96
96
  continue
97
97
  }
98
98
  let content = fs.readFileSync(from, 'utf8')
99
99
  for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value)
100
100
  F.atomicWrite(to, content)
101
101
  if (entry.name.endsWith('.js')) fs.chmodSync(to, 0o755)
102
- console.log(`+ ${to}`)
102
+ if (!quiet) console.log(`+ ${to}`)
103
103
  }
104
104
  }
105
105
  }
@@ -143,29 +143,22 @@ function providerNames() {
143
143
  } catch { return [] }
144
144
  }
145
145
 
146
- function init(target) {
147
- if (!target) fail('Falta <destino>.', 2)
148
- const mode = option('--mode', 'embedded')
149
- if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
150
- const name = option('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
151
- const root = path.resolve(target)
152
- const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
153
- if (existing.length && !process.argv.includes('--force')) {
154
- fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
155
- }
146
+ // El andamiaje de una instancia, sin leer argv. `init` es la cáscara que traduce banderas a esto, y
147
+ // el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
148
+ // cómo venga escrita la línea de comandos.
149
+ function scaffold(root, { name, mode, force = false, quiet = false }) {
156
150
  copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
157
151
  '{{PROJECT_NAME}}': name,
158
152
  '{{MODE}}': mode,
159
153
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
160
- }, process.argv.includes('--force'), providerNames())
154
+ }, force, providerNames(), quiet)
161
155
  // No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
162
156
  // el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando los dos únicos archivos
163
157
  // que existen dejaba `.github/workflows/` vacío en cada instancia.
164
- const preserve = process.argv.includes('--force')
165
158
  copyRuntime(
166
159
  path.join(PROJECT_ROOT, 'automatization', 'hooks'),
167
160
  path.join(root, 'automatization', 'hooks'),
168
- preserve,
161
+ force,
169
162
  root,
170
163
  )
171
164
  const version = require(path.join(PROJECT_ROOT, 'package.json')).version
@@ -188,6 +181,44 @@ function init(target) {
188
181
  config.cauceVersion = version
189
182
  config.$schema = 'node_modules/@ingeniomaps/cauce/engine/schemas/ops-config.schema.json'
190
183
  F.atomicWriteJson(configFile, config)
184
+ return root
185
+ }
186
+
187
+ // Un banco de trabajo desechable donde un cargo del catálogo puede realmente trabajar.
188
+ //
189
+ // Hace falta porque el toolkit no es una raíz ops: no tiene `planning/`, y no puede tenerlo —el único
190
+ // `planning/` que vive acá es `template/planning`, el molde que se distribuye—. Un cargo cuya entrega
191
+ // es una épica o una entrada de INBOX no tiene dónde escribir, así que se niega. Con razón, y su caso
192
+ // lo cuenta como fallo: eso midió una configuración, no al cargo.
193
+ //
194
+ // Se recrea entero en cada corrida. Reutilizarlo dejaría que lo que un cargo escribió el lunes sea
195
+ // contexto del que responde el martes, y dos corridas del mismo caso dejarían de ser comparables.
196
+ // Queda en disco al terminar, gitignorado, porque después de un veredicto raro lo primero que uno
197
+ // quiere es mirar qué escribió el cargo.
198
+ function evaluationBench(root) {
199
+ const dir = path.join(root, '.cauce-eval')
200
+ fs.rmSync(dir, { recursive: true, force: true })
201
+ scaffold(dir, { name: 'Banco de evaluación', mode: 'sidecar', quiet: true })
202
+ // El motor por symlink: la misma resolución que en una instancia real —`node_modules/@ingeniomaps`—
203
+ // sin pagar un `npm install` por corrida. El cargo llega a un banco donde el CLI funciona.
204
+ const scope = path.join(dir, 'node_modules', '@ingeniomaps')
205
+ fs.mkdirSync(scope, { recursive: true })
206
+ fs.symlinkSync(PROJECT_ROOT, path.join(scope, 'cauce'), 'dir')
207
+ return dir
208
+ }
209
+
210
+ function init(target) {
211
+ if (!target) fail('Falta <destino>.', 2)
212
+ const mode = option('--mode', 'embedded')
213
+ if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
214
+ const name = option('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
215
+ const root = path.resolve(target)
216
+ const force = process.argv.includes('--force')
217
+ const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
218
+ if (existing.length && !force) {
219
+ fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
220
+ }
221
+ scaffold(root, { name, mode, force })
191
222
  console.log(`\n✓ ${name}: sistema ops creado en ${root}`)
192
223
  console.log(' siguiente: npm install (el motor viene de la dependencia)')
193
224
  console.log(` siguiente: node ${path.join(root, 'tools', 'ops.js')} check ${path.join(root, 'planning')}`)
@@ -850,6 +881,17 @@ function learn(agent) {
850
881
 
851
882
  function evaluate(agent) {
852
883
  const root = opsRoot()
884
+ // El banco sólo tiene sentido acá: en una empresa el cargo que se evalúa es suyo —propio o
885
+ // adoptado— y su `planning/` ya es el lugar legítimo donde trabajar.
886
+ if (process.argv.includes('--bench')) {
887
+ let mode = ''
888
+ try { mode = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8')).mode } catch { /* sin config */ }
889
+ if (mode !== 'toolkit') {
890
+ fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: si es del '
891
+ + `catálogo, adoptalo primero con "ops agents fork ${agent}".`, 2)
892
+ }
893
+ return console.log(evaluationBench(root))
894
+ }
853
895
  try {
854
896
  // Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
855
897
  // máquina, no de persona.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "type": "commonjs",
6
6
  "bin": {