@ingeniomaps/cauce 0.16.1 → 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 +55 -0
- package/README.md +20 -0
- package/automatization/workflows/agent-eval.js +44 -18
- package/engine/agents/fork.js +152 -0
- package/engine/cli/ops.js +85 -20
- package/engine/core/manifest.js +13 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,61 @@ 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
|
+
|
|
39
|
+
## [0.17.0] - 2026-08-16
|
|
40
|
+
|
|
41
|
+
### Agregado
|
|
42
|
+
|
|
43
|
+
- **`ops agents fork <cargo>`: una empresa se lleva un cargo del catálogo y lo mantiene desde su
|
|
44
|
+
carpeta.** El tercer camino ya resolvía —un cargo propio con el mismo slug tapa al del sistema, en el
|
|
45
|
+
listado, en `learn` y en el puntero que instala el runner—, pero llegar hasta ahí era copiar a mano, y
|
|
46
|
+
eso sale mal de una forma que no se nota: se agarra el `SKILL.md`, que es lo que se ve, y quedan atrás
|
|
47
|
+
los casos adversariales, las fuentes y el modelo operativo. El cargo responde igual y ya no se puede
|
|
48
|
+
evaluar, sin ningún aviso.
|
|
49
|
+
|
|
50
|
+
No se heredan los informes de aprendizaje, las propuestas ni los veredictos de evaluación. Un veredicto
|
|
51
|
+
pertenece al contrato que lo ganó, y el fork nace para dejar de ser ese contrato; una propuesta
|
|
52
|
+
pendiente arrastraría a la empresa a firmar una decisión que era nuestra.
|
|
53
|
+
|
|
54
|
+
En el toolkit se niega: acá el catálogo se edita, no se lo copia. Dejarlo pasar creaba un duplicado que
|
|
55
|
+
tapaba al original, y el trabajo siguiente se hacía sobre la copia mientras la versión que se publica
|
|
56
|
+
quedaba quieta.
|
|
57
|
+
|
|
58
|
+
- **`check` y `upgrade` avisan cuando el cargo que forkeaste mejoró río arriba.** El mecanismo ya existía
|
|
59
|
+
para ADRs y reglas —«sobrescribir es legítimo; lo que no puede pasar es que ocurra en silencio»— pero
|
|
60
|
+
no cubría los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
|
|
61
|
+
|
|
62
|
+
Se compara contra los digests guardados al forkear, nunca contra la copia: la copia está editada a
|
|
63
|
+
propósito, así que medir contra ella devolvería «todo cambió» desde el primer ajuste. Editar lo propio
|
|
64
|
+
no dispara nada, y esa mitad es la que decide si el aviso se lee o se ignora.
|
|
65
|
+
|
|
11
66
|
## [0.16.1] - 2026-08-16
|
|
12
67
|
|
|
13
68
|
### Corregido
|
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
|
|
|
74
74
|
| `ops tree <planning>` | Muestra roadmap, backlog, WIP, inbox y done sin mutar nada. |
|
|
75
75
|
| `ops context <planning>` | Emite el contexto mínimo de la tarea vigente para un runner. |
|
|
76
76
|
| `ops agents list <ops-root>` | Lista los cargos visibles resolviendo la precedencia. |
|
|
77
|
+
| `ops agents fork <cargo>` | Copia un cargo del catálogo a la empresa, que pasa a mantenerlo. |
|
|
77
78
|
| `ops upgrade <ops-root>` | Actualiza `system/` y el runtime sin tocar lo del proyecto. |
|
|
78
79
|
| `ops archive <planning> <NNN>` | Archiva el DONE de una épica cerrada de forma idempotente. |
|
|
79
80
|
| `ops learn <agent>` | Prepara el informe semanal que completa la automatización de Codex. |
|
|
@@ -183,6 +184,25 @@ todas las empresas: investigarla una vez y bien es mejor que repetirla en cada i
|
|
|
183
184
|
Por eso `learn` falla si lo corrés sobre un cargo del catálogo dentro de una instancia: escribiría en
|
|
184
185
|
el paquete y se perdería. El ciclo mensual de aprendizaje tampoco se distribuye — vive sólo acá.
|
|
185
186
|
|
|
187
|
+
#### Quedarse con una versión propia de un cargo del catálogo
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm run ops -- agents fork product-manager
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Copia el cargo entero a `agents/roles/<slug>/` y desde ahí lo mantenés vos: `learn`, `evaluate` y el
|
|
194
|
+
puntero que instala el runner pasan a resolver contra tu copia. **Copiarlo a mano no es equivalente**
|
|
195
|
+
—se agarra el `SKILL.md`, que es lo que se ve, y quedan atrás los casos, las fuentes y el modelo
|
|
196
|
+
operativo: el cargo responde igual y ya no se puede evaluar—.
|
|
197
|
+
|
|
198
|
+
Lo que no viaja son los informes de aprendizaje, las propuestas y los veredictos de evaluación. Un
|
|
199
|
+
veredicto pertenece al contrato que lo ganó, y el fork nace para dejar de ser ese contrato.
|
|
200
|
+
|
|
201
|
+
A partir de ahí tu copia deja de recibir las mejoras del catálogo, que es lo que elegiste, pero no en
|
|
202
|
+
silencio: `check` y `upgrade` avisan cuando el original cambia río arriba. Editar tu propia copia no
|
|
203
|
+
dispara nada — se compara contra lo que el catálogo tenía el día del fork, no contra lo que vos
|
|
204
|
+
escribiste después.
|
|
205
|
+
|
|
186
206
|
### Versionado
|
|
187
207
|
|
|
188
208
|
Como `upgrade` reemplaza `system/` sin pedir confirmación, un cambio en el protocolo, en una regla del
|
|
@@ -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` +
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Llevarse un cargo del catálogo a la carpeta de la empresa para mantenerlo desde ahí.
|
|
4
|
+
//
|
|
5
|
+
// Existe porque copiar a mano sale mal de una forma que no se nota: quien copia agarra el `SKILL.md`
|
|
6
|
+
// —que es lo que se ve— y deja atrás los casos adversariales, las fuentes de aprendizaje y el modelo
|
|
7
|
+
// operativo. Queda un cargo que responde igual y ya no se puede evaluar, sin ningún aviso.
|
|
8
|
+
//
|
|
9
|
+
// **Qué no se hereda.** Los informes de aprendizaje, las propuestas y los veredictos de evaluación se
|
|
10
|
+
// quedan en el catálogo. No es prolijidad: un veredicto pertenece al contrato que lo ganó, y el fork
|
|
11
|
+
// nace para dejar de ser ese contrato. Heredarlos le daría a la copia una garantía que no rindió, y
|
|
12
|
+
// una propuesta pendiente arrastraría a la empresa a firmar una decisión que era nuestra.
|
|
13
|
+
//
|
|
14
|
+
// Lo que sí se hereda entero es el contrato y todo lo que lo hace verificable: casos, conductas
|
|
15
|
+
// esperadas, fuentes, automatización y referencias. La empresa recibe un cargo evaluable desde el
|
|
16
|
+
// primer minuto, y a partir de ahí lo mantiene.
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs')
|
|
19
|
+
const path = require('node:path')
|
|
20
|
+
|
|
21
|
+
const catalog = require('./catalog')
|
|
22
|
+
const manifest = require('../core/manifest')
|
|
23
|
+
const ownership = require('../core/ownership')
|
|
24
|
+
|
|
25
|
+
// Se comparan contra la ruta relativa dentro del cargo. `_template.md` sobrevive en las dos carpetas
|
|
26
|
+
// porque es andamiaje del mecanismo, no un artefacto de nadie.
|
|
27
|
+
function inherited(relative) {
|
|
28
|
+
if (/^learning\/(reports|proposals)\//.test(relative)) return path.basename(relative) === '_template.md'
|
|
29
|
+
if (relative.startsWith('evaluations/results/')) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tree(dir, prefix = '') {
|
|
34
|
+
const found = []
|
|
35
|
+
let list = []
|
|
36
|
+
try { list = fs.readdirSync(dir, { withFileTypes: true }) } catch { return found }
|
|
37
|
+
for (const entry of list) {
|
|
38
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
39
|
+
if (entry.isDirectory()) found.push(...tree(path.join(dir, entry.name), relative))
|
|
40
|
+
else found.push(relative)
|
|
41
|
+
}
|
|
42
|
+
return found.sort()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// La misma resolución que usa todo lo demás —paquete primero, toolkit después— en vez de deducirla
|
|
46
|
+
// del directorio del motor: la versión que interesa es la del catálogo del que sale la copia.
|
|
47
|
+
function packageVersion(root) {
|
|
48
|
+
const file = ownership.packagePath(root, 'package.json')
|
|
49
|
+
if (!file) return ''
|
|
50
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')).version || '' } catch { return '' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// La fila deja el límite por escrito: arriba, cómo llegó el contrato a ser lo que es —decisiones que
|
|
54
|
+
// tomamos nosotros—; abajo, lo que decida la empresa. Sin ese renglón, la primera fila propia parece
|
|
55
|
+
// continuación de una conversación ajena.
|
|
56
|
+
function markHistory(target, slug, version, date) {
|
|
57
|
+
const file = path.join(target, 'learning', 'HISTORY.md')
|
|
58
|
+
if (!fs.existsSync(file)) return
|
|
59
|
+
const origen = version ? `del catálogo de Cauce ${version}` : 'del catálogo de Cauce'
|
|
60
|
+
const row = `| ${date} | — | Copiado ${origen} | — | ${slug} pasa a mantenerlo esta empresa |\n`
|
|
61
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
62
|
+
fs.writeFileSync(file, content.endsWith('\n') ? content + row : `${content}\n${row}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `date` entra por parámetro en vez de leerse acá: la salida tiene que ser reproducible en una prueba.
|
|
66
|
+
function fork(root, slug, date) {
|
|
67
|
+
// En el toolkit no hay a quién copiarle: el catálogo se mantiene acá, directo. Dejarlo pasar creaba
|
|
68
|
+
// un duplicado en `agents/roles/` que tapaba al original, y el trabajo siguiente se hacía sobre la
|
|
69
|
+
// copia mientras la versión que se publica quedaba quieta.
|
|
70
|
+
try {
|
|
71
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
72
|
+
if (config.mode === 'toolkit') {
|
|
73
|
+
throw new Error(`${slug} vive acá: en el toolkit se edita el catálogo, no se lo copia`)
|
|
74
|
+
}
|
|
75
|
+
} catch (error) { if (error instanceof Error && error.message.includes('vive acá')) throw error }
|
|
76
|
+
|
|
77
|
+
const found = catalog.find(root, slug)
|
|
78
|
+
if (!found.system) {
|
|
79
|
+
throw new Error(`${slug} ya lo mantiene esta empresa: ${path.relative(root, found.dir)}`)
|
|
80
|
+
}
|
|
81
|
+
const type = path.basename(path.dirname(path.dirname(found.dir)))
|
|
82
|
+
const target = path.join(root, 'agents', type, slug)
|
|
83
|
+
if (fs.existsSync(target)) throw new Error(`${path.relative(root, target)} ya existe`)
|
|
84
|
+
|
|
85
|
+
const files = tree(found.dir).filter(inherited)
|
|
86
|
+
if (!files.includes('SKILL.md')) throw new Error(`${slug} no tiene SKILL.md: no hay contrato que copiar`)
|
|
87
|
+
|
|
88
|
+
// El digest sale del origen, no de la copia. Es lo que el catálogo tenía al momento del fork, y es
|
|
89
|
+
// contra eso que después se responde «esto mejoró río arriba»; medirlo sobre la copia lo ataría a
|
|
90
|
+
// cualquier cosa que le hagamos acá —empezando por la fila de historial que se escribe abajo—.
|
|
91
|
+
const digests = {}
|
|
92
|
+
for (const relative of files) {
|
|
93
|
+
const from = path.join(found.dir, relative)
|
|
94
|
+
const to = path.join(target, relative)
|
|
95
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
96
|
+
fs.copyFileSync(from, to)
|
|
97
|
+
digests[relative] = manifest.digest(from)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const version = packageVersion(root)
|
|
101
|
+
markHistory(target, slug, version, date)
|
|
102
|
+
|
|
103
|
+
const forks = { ...manifest.readForks(root) }
|
|
104
|
+
forks[slug] = { type, version, files: digests }
|
|
105
|
+
manifest.write(root, null, null, forks)
|
|
106
|
+
|
|
107
|
+
return { slug, type, dir: target, files, version, skipped: tree(found.dir).filter((one) => !inherited(one)) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Qué cambió en el catálogo desde que la empresa se llevó su copia.
|
|
111
|
+
//
|
|
112
|
+
// El fork es legítimo y esperado —para eso está el comando—, pero deja de recibir las mejoras del
|
|
113
|
+
// toolkit, y eso no puede pasar en silencio: la empresa decidió mantener un cargo, no quedarse sin
|
|
114
|
+
// enterarse de que el original mejoró. Nadie va a comparar 14 archivos a mano en cada actualización.
|
|
115
|
+
//
|
|
116
|
+
// Se compara contra los digests guardados al forkear, nunca contra la copia: la copia está editada a
|
|
117
|
+
// propósito, así que medir contra ella devolvería «todo cambió» desde el primer ajuste. Y el aviso no
|
|
118
|
+
// dice qué hacer —integrar o no es decisión de la empresa—, sólo que hay algo que mirar.
|
|
119
|
+
function drift(root) {
|
|
120
|
+
const forks = manifest.readForks(root)
|
|
121
|
+
const catalogDir = ownership.packageDir(root, 'agents')
|
|
122
|
+
const found = []
|
|
123
|
+
for (const [slug, record] of Object.entries(forks)) {
|
|
124
|
+
if (!record || !record.files) continue
|
|
125
|
+
const source = catalogDir ? path.join(catalogDir, record.type || 'roles', 'system', slug) : ''
|
|
126
|
+
if (!source || !fs.existsSync(source)) continue
|
|
127
|
+
const current = tree(source).filter(inherited)
|
|
128
|
+
const changed = []
|
|
129
|
+
const removed = []
|
|
130
|
+
for (const [relative, recorded] of Object.entries(record.files)) {
|
|
131
|
+
if (!current.includes(relative)) { removed.push(relative); continue }
|
|
132
|
+
if (manifest.digest(path.join(source, relative)) !== recorded) changed.push(relative)
|
|
133
|
+
}
|
|
134
|
+
const added = current.filter((relative) => !(relative in record.files))
|
|
135
|
+
if (changed.length || added.length || removed.length) {
|
|
136
|
+
found.push({ slug, version: record.version || '', changed, added, removed })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return found.sort((left, right) => left.slug.localeCompare(right.slug))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Una línea por fork, para que `check` y `upgrade` digan lo mismo con las mismas palabras.
|
|
143
|
+
function driftLine(entry) {
|
|
144
|
+
const parts = []
|
|
145
|
+
if (entry.changed.length) parts.push(`${entry.changed.length} archivo(s) cambiaron`)
|
|
146
|
+
if (entry.added.length) parts.push(`${entry.added.length} nuevo(s)`)
|
|
147
|
+
if (entry.removed.length) parts.push(`${entry.removed.length} retirado(s)`)
|
|
148
|
+
const desde = entry.version ? ` desde ${entry.version}` : ''
|
|
149
|
+
return `${entry.slug}: tu copia no recibe mejoras del catálogo y ahí${desde} ${parts.join(', ')}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { drift, driftLine, fork, inherited }
|
package/engine/cli/ops.js
CHANGED
|
@@ -49,8 +49,9 @@ 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
|
+
ops agents fork <cargo> [ops-root]
|
|
54
55
|
ops team list
|
|
55
56
|
ops team check <team>
|
|
56
57
|
ops team show <team>`)
|
|
@@ -78,7 +79,7 @@ function option(name, fallback = '') {
|
|
|
78
79
|
return index >= 0 ? process.argv[index + 1] || fallback : fallback
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function copyTemplate(source, target, replacements, force, skip = []) {
|
|
82
|
+
function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
|
|
82
83
|
F.assertNoSymlinkPath(path.dirname(target), target)
|
|
83
84
|
fs.mkdirSync(target, { recursive: true })
|
|
84
85
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
@@ -87,18 +88,18 @@ function copyTemplate(source, target, replacements, force, skip = []) {
|
|
|
87
88
|
// npm no incluye un `.gitignore` dentro de un tarball, así que viaja sin punto y se restituye
|
|
88
89
|
// acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
|
|
89
90
|
const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
|
|
90
|
-
if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip)
|
|
91
|
+
if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
|
|
91
92
|
else {
|
|
92
93
|
if (fs.existsSync(to)) {
|
|
93
94
|
if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
|
|
94
|
-
console.log(`= conservado ${to}`)
|
|
95
|
+
if (!quiet) console.log(`= conservado ${to}`)
|
|
95
96
|
continue
|
|
96
97
|
}
|
|
97
98
|
let content = fs.readFileSync(from, 'utf8')
|
|
98
99
|
for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value)
|
|
99
100
|
F.atomicWrite(to, content)
|
|
100
101
|
if (entry.name.endsWith('.js')) fs.chmodSync(to, 0o755)
|
|
101
|
-
console.log(`+ ${to}`)
|
|
102
|
+
if (!quiet) console.log(`+ ${to}`)
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -142,29 +143,22 @@ function providerNames() {
|
|
|
142
143
|
} catch { return [] }
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const name = option('--name', path.basename(path.resolve(target)).replace(/-ops$/, ''))
|
|
150
|
-
const root = path.resolve(target)
|
|
151
|
-
const existing = fs.existsSync(root) ? fs.readdirSync(root) : []
|
|
152
|
-
if (existing.length && !process.argv.includes('--force')) {
|
|
153
|
-
fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
|
|
154
|
-
}
|
|
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 }) {
|
|
155
150
|
copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
|
|
156
151
|
'{{PROJECT_NAME}}': name,
|
|
157
152
|
'{{MODE}}': mode,
|
|
158
153
|
'{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
|
|
159
|
-
},
|
|
154
|
+
}, force, providerNames(), quiet)
|
|
160
155
|
// No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
|
|
161
156
|
// el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando los dos únicos archivos
|
|
162
157
|
// que existen dejaba `.github/workflows/` vacío en cada instancia.
|
|
163
|
-
const preserve = process.argv.includes('--force')
|
|
164
158
|
copyRuntime(
|
|
165
159
|
path.join(PROJECT_ROOT, 'automatization', 'hooks'),
|
|
166
160
|
path.join(root, 'automatization', 'hooks'),
|
|
167
|
-
|
|
161
|
+
force,
|
|
168
162
|
root,
|
|
169
163
|
)
|
|
170
164
|
const version = require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
@@ -187,6 +181,44 @@ function init(target) {
|
|
|
187
181
|
config.cauceVersion = version
|
|
188
182
|
config.$schema = 'node_modules/@ingeniomaps/cauce/engine/schemas/ops-config.schema.json'
|
|
189
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 })
|
|
190
222
|
console.log(`\n✓ ${name}: sistema ops creado en ${root}`)
|
|
191
223
|
console.log(' siguiente: npm install (el motor viene de la dependencia)')
|
|
192
224
|
console.log(` siguiente: node ${path.join(root, 'tools', 'ops.js')} check ${path.join(root, 'planning')}`)
|
|
@@ -300,6 +332,9 @@ function check(dir) {
|
|
|
300
332
|
for (const override of O.overrides(path.resolve(root, '..'))) {
|
|
301
333
|
warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} (override explícito)`)
|
|
302
334
|
}
|
|
335
|
+
// Misma regla para los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
|
|
336
|
+
const FK = require('../agents/fork')
|
|
337
|
+
for (const entry of FK.drift(path.resolve(root, '..'))) warnings.push(FK.driftLine(entry))
|
|
303
338
|
|
|
304
339
|
if (process.argv.includes('--json')) {
|
|
305
340
|
console.log(JSON.stringify({
|
|
@@ -604,6 +639,10 @@ function upgrade(dir) {
|
|
|
604
639
|
for (const name of runners) {
|
|
605
640
|
console.log(` reinstalá tu runner para que el wiring quede al día: make install-${name}`)
|
|
606
641
|
}
|
|
642
|
+
// Después de aplicar, no antes: recién acá el paquete tiene la versión nueva y la comparación dice
|
|
643
|
+
// algo. Es además el momento en que alguien está mirando qué le trajo la actualización.
|
|
644
|
+
const FK = require('../agents/fork')
|
|
645
|
+
for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
|
|
607
646
|
}
|
|
608
647
|
|
|
609
648
|
// Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
|
|
@@ -614,7 +653,22 @@ function opsRoot(dir) {
|
|
|
614
653
|
return path.resolve(dir || process.env.OPS_ROOT || '.')
|
|
615
654
|
}
|
|
616
655
|
|
|
617
|
-
function
|
|
656
|
+
function agentsFork(slug, dir) {
|
|
657
|
+
const root = opsRoot(dir)
|
|
658
|
+
if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', 2)
|
|
659
|
+
let result
|
|
660
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
661
|
+
try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message, 2) }
|
|
662
|
+
console.log(`+ ${path.relative(root, result.dir)} (${result.files.length} archivo(s))`)
|
|
663
|
+
if (result.skipped.length) {
|
|
664
|
+
console.log(` quedan en el catálogo: ${result.skipped.length} artefacto(s) que ganó su versión`)
|
|
665
|
+
}
|
|
666
|
+
console.log(` copiado de Cauce ${result.version || '(versión desconocida)'}; desde ahora lo mantenés vos`)
|
|
667
|
+
console.log(` reinstalá tu runner para que ${slug} apunte a tu copia`)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function agents(action, dir, extra) {
|
|
671
|
+
if (action === 'fork') return agentsFork(dir, extra)
|
|
618
672
|
if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
|
|
619
673
|
const root = opsRoot(dir)
|
|
620
674
|
// Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
|
|
@@ -827,6 +881,17 @@ function learn(agent) {
|
|
|
827
881
|
|
|
828
882
|
function evaluate(agent) {
|
|
829
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
|
+
}
|
|
830
895
|
try {
|
|
831
896
|
// Los casos, para que un recorrido los ejecute. Sin `--json` no tiene sentido: es entrada de
|
|
832
897
|
// máquina, no de persona.
|
|
@@ -884,7 +949,7 @@ async function main() {
|
|
|
884
949
|
else if (command === 'tree') tree(arg[1])
|
|
885
950
|
else if (command === 'context') context(arg[1])
|
|
886
951
|
else if (command === 'upgrade') upgrade(arg[1])
|
|
887
|
-
else if (command === 'agents') agents(arg[1], arg[2])
|
|
952
|
+
else if (command === 'agents') agents(arg[1], arg[2], arg[3])
|
|
888
953
|
else if (command === 'archive') archive(arg[1], arg[2])
|
|
889
954
|
else if (command === 'integration') {
|
|
890
955
|
await integration(arg[1], arg[2], arg[3], arg[4])
|
package/engine/core/manifest.js
CHANGED
|
@@ -30,17 +30,24 @@ function readAll(root) {
|
|
|
30
30
|
return {
|
|
31
31
|
files: typeof data.files === 'object' && data.files ? data.files : {},
|
|
32
32
|
runners: typeof data.runners === 'object' && data.runners ? data.runners : {},
|
|
33
|
+
forks: typeof data.forks === 'object' && data.forks ? data.forks : {},
|
|
33
34
|
}
|
|
34
|
-
} catch { return { files: {}, runners: {} } }
|
|
35
|
+
} catch { return { files: {}, runners: {}, forks: {} } }
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function read(root) { return readAll(root).files }
|
|
38
39
|
|
|
39
40
|
function readRunners(root) { return readAll(root).runners }
|
|
40
41
|
|
|
42
|
+
// Tercera sección, y de otra naturaleza que las dos anteriores: `files` y `runners` registran lo que
|
|
43
|
+
// Cauce entregó, mientras `forks` registra lo que la empresa se llevó. Guarda el contenido del cargo
|
|
44
|
+
// del sistema **en el momento de la copia**, que es lo único contra lo que se puede decir después
|
|
45
|
+
// «esto mejoró río arriba»: comparar contra el fork mismo mediría las ediciones de la empresa.
|
|
46
|
+
function readForks(root) { return readAll(root).forks }
|
|
47
|
+
|
|
41
48
|
// Cada sección que no se pasa se conserva: `upgrade` no sabe del wiring y `install` no sabe de la
|
|
42
49
|
// instancia, y ninguno de los dos debería borrar lo que el otro anotó.
|
|
43
|
-
function write(root, files, runners) {
|
|
50
|
+
function write(root, files, runners, forks) {
|
|
44
51
|
const current = readAll(root)
|
|
45
52
|
const target = path.join(root, FILE)
|
|
46
53
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
@@ -51,6 +58,7 @@ function write(root, files, runners) {
|
|
|
51
58
|
version: 1,
|
|
52
59
|
files: ordered(files || current.files),
|
|
53
60
|
runners: ordered(runners || current.runners),
|
|
61
|
+
forks: ordered(forks || current.forks),
|
|
54
62
|
}
|
|
55
63
|
fs.writeFileSync(target, `${JSON.stringify(data, null, 2)}\n`)
|
|
56
64
|
}
|
|
@@ -85,4 +93,6 @@ function edited(root, relative, files) {
|
|
|
85
93
|
})
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
module.exports = {
|
|
96
|
+
module.exports = {
|
|
97
|
+
FILE, digest, digestText, edited, prune, read, readForks, readRunners, record, write,
|
|
98
|
+
}
|