@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 +28 -0
- package/automatization/workflows/agent-eval.js +44 -18
- package/engine/cli/ops.js +60 -18
- package/package.json +1 -1
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
|
-
// **
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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 ${
|
|
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í ${
|
|
153
|
-
`
|
|
154
|
-
`
|
|
155
|
-
`
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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.
|