@ingeniomaps/cauce 0.4.0 → 0.5.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 +5 -4
- package/automatization/hooks/run-hook.sh +4 -0
- package/engine/automation/index.js +173 -34
- package/engine/cli/ops.js +22 -21
- package/engine/core/manifest.js +37 -8
- package/engine/core/ownership.js +32 -11
- package/package.json +1 -1
- package/template/AGENTS.md +3 -2
- package/template/automatization/README.md +23 -8
- package/template/automatization/runners/README.md +0 -13
- package/template/automatization/workflows/.gitkeep +0 -1
- /package/{teams → template/teams}/000-template.md +0 -0
- /package/{teams → template/teams}/README.md +0 -0
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.5.0] - 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Cambiado
|
|
14
|
+
|
|
15
|
+
- **Los adaptadores de runner y los workflows tampoco se copian al proyecto.** Cierran el mismo
|
|
16
|
+
criterio que ya rige para cargos y equipos: los lee el motor, no la empresa. `automation install`,
|
|
17
|
+
`check` y `doctor` los resuelven desde la dependencia npm, o desde `.ops/` cuando el repo no usa
|
|
18
|
+
npm. Nadie los editaba —la lista de runners es cerrada, así que una empresa ni siquiera podía
|
|
19
|
+
agregar el suyo— y `automatization/workflows/` estaba además duplicado dentro de cada instancia,
|
|
20
|
+
idéntico a lo que `automation install` deja en `.claude/workflows/`.
|
|
21
|
+
- `upgrade` retira `automatization/runners/` y `automatization/workflows/` de las instancias que los
|
|
22
|
+
arrastran de una versión anterior. Un guard propio en `automatization/hooks/` no se toca.
|
|
23
|
+
- `automatization/hooks/` se queda en el proyecto, y esto no es una excepción arbitraria: la
|
|
24
|
+
configuración de cada runner nombra cada guard por ruta literal y no sabe resolver en cascada. Ahí
|
|
25
|
+
es también donde una empresa agrega el suyo.
|
|
26
|
+
|
|
27
|
+
- **En modo `sidecar`, `automation install` instala en la carpeta de la compañía, no dentro del repo
|
|
28
|
+
ops.** El repo ops coordina varios repos de producto y es hermano de ellos, así que instalar el
|
|
29
|
+
runner adentro lo dejaba sin ver una sola línea de código: el dev que abría su herramienta donde
|
|
30
|
+
está el código no tenía guards, ni cargos, ni workflows. Las rutas de guards y los punteros de cada
|
|
31
|
+
cargo se reescriben con el prefijo del repo ops al instalar.
|
|
32
|
+
|
|
33
|
+
### Corregido
|
|
34
|
+
|
|
35
|
+
- **Una mejora del toolkit en el wiring ahora llega a un runner ya instalado.** `install` conservaba
|
|
36
|
+
cualquier archivo existente, así que un workflow o un `CLAUDE.md` mejorado río arriba no llegaba
|
|
37
|
+
nunca. Peor: si el archivo difería, `install` fallaba —`existe y difiere de la fuente canónica`— y
|
|
38
|
+
`doctor` lo reportaba como error, dejando a la empresa sin salida salvo borrar a mano. Ahora el
|
|
39
|
+
registro de entrega distingue las dos cosas que antes se veían iguales: lo que la empresa editó se
|
|
40
|
+
conserva o detiene la instalación, y lo que sólo cambió río arriba se actualiza.
|
|
41
|
+
- `automation check` compara los guards de la instancia contra los del paquete. Existir y ser
|
|
42
|
+
ejecutable no alcanzaba: un guard viejo no falla, **deja de proteger en silencio**, y `check`,
|
|
43
|
+
`doctor` y `upgrade` daban verde igual porque sólo miraban el número de versión. Ahora bloquea la
|
|
44
|
+
instalación del runner y dice qué correr; distingue además el guard que la empresa editó del que
|
|
45
|
+
simplemente quedó atrás.
|
|
46
|
+
- `upgrade` recuerda reinstalar los runners: el wiring vive fuera de la instancia y lo escribe otro
|
|
47
|
+
comando, así que sin el aviso una mejora se quedaba en el paquete.
|
|
48
|
+
- Los guards resuelven la raíz ops por su cuenta. `findOpsRoot` sólo sube por el árbol, y en sidecar
|
|
49
|
+
la raíz ops es un *hermano* de los repos de producto: desde ahí no la encontraba, devolvía vacío y
|
|
50
|
+
el guard permitía todo **en silencio**. `run-hook.sh` ya sabía dónde vive; ahora lo exporta.
|
|
51
|
+
- `automatization/README.md` y `automatization/AGENTS.md` se actualizan con el toolkit. Los escribe
|
|
52
|
+
Cauce y envejecían en cada instancia: después de retirar `runners/` y `workflows/`, el README de la
|
|
53
|
+
empresa seguía explicando cómo usar dos carpetas que ya no existían.
|
|
54
|
+
- El registro de entrega olvida lo que ya no está en disco. Sólo crecía: al retirar una ruta dejaba
|
|
55
|
+
su digest para siempre, y el día que un nombre se reutilizara la entrega nueva se habría leído como
|
|
56
|
+
una edición local y detenido la actualización.
|
|
57
|
+
|
|
58
|
+
## [0.4.1] - 2026-08-15
|
|
59
|
+
|
|
60
|
+
### Corregido
|
|
61
|
+
|
|
62
|
+
- Al mover los equipos al paquete se fue con ellos su documentación, así que una instancia nueva se
|
|
63
|
+
quedaba sin `teams/README.md` ni la plantilla: sabía que podía escribir equipos propios, pero no
|
|
64
|
+
con qué contrato. Lo que le habla a la empresa viaja con la instancia aunque la colección no.
|
|
65
|
+
|
|
11
66
|
## [0.4.0] - 2026-08-15
|
|
12
67
|
|
|
13
68
|
### Cambiado
|
package/README.md
CHANGED
|
@@ -157,10 +157,11 @@ Cada colección adaptable separa lo que actualiza el toolkit de lo que escribe e
|
|
|
157
157
|
| `teams/` | composiciones que vienen con Cauce | los equipos propios |
|
|
158
158
|
| `agents/<tipo>/` | *(en el paquete, no se copia)* | los cargos propios |
|
|
159
159
|
|
|
160
|
-
`automatization/hooks/`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
`automatization/hooks/` no tiene `system/`: es runtime que se reemplaza entero. No hace falta, porque lo
|
|
161
|
+
que un proyecto necesita ya funciona sin editarlo — un guard propio convive y sobrevive, y desactivar uno
|
|
162
|
+
del toolkit es quitarlo de la configuración del runner, que es del proyecto. Editar uno existente detiene
|
|
163
|
+
el `upgrade` antes de pisarlo. Los hooks se quedan en el proyecto porque esa configuración los nombra por
|
|
164
|
+
ruta literal; los adaptadores de runner y los workflows, que sólo lee el motor, viajan en el paquete.
|
|
164
165
|
|
|
165
166
|
Un archivo propio con el mismo nombre o ID que uno de `system/` lo reemplaza: el del proyecto manda y
|
|
166
167
|
`check` lo reporta como override explícito. Así una mejora del proceso no obliga a forkear el archivo,
|
|
@@ -4,6 +4,10 @@ set -u
|
|
|
4
4
|
hook_name=${1:-}
|
|
5
5
|
hook_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
6
6
|
ops_root=$(CDPATH= cd -- "$hook_dir/../.." && pwd)
|
|
7
|
+
# El guard sabe dónde vive; quien lo invoca, no necesariamente. En modo sidecar la herramienta se
|
|
8
|
+
# abre en la carpeta de la compañía y la raíz ops es un hermano, que ninguna búsqueda hacia arriba
|
|
9
|
+
# encuentra: sin esto el guard no halla `ops.config.json` y deja pasar todo en silencio.
|
|
10
|
+
export OPS_ROOT="$ops_root"
|
|
7
11
|
# Mismo orden que tools/ops.js: primero la dependencia npm, después la copia local, y por último
|
|
8
12
|
# el propio repositorio del toolkit. Un guard que no encuentra su motor bloquea, nunca permite.
|
|
9
13
|
runner=""
|
|
@@ -8,14 +8,30 @@ const H = require('../hooks/run')
|
|
|
8
8
|
const P = require('../planning/parser')
|
|
9
9
|
const catalog = require('../agents/catalog')
|
|
10
10
|
const O = require('../core/ownership')
|
|
11
|
+
const M = require('../core/manifest')
|
|
11
12
|
|
|
12
13
|
const RUNNER_NAMES = ['claude', 'codex', 'gemini', 'antigravity']
|
|
13
14
|
|
|
15
|
+
// Adaptadores y workflows viven en el paquete, no en la instancia: son definiciones que el motor
|
|
16
|
+
// consume y que ninguna empresa edita —`RUNNER_NAMES` es cerrado, así que ni siquiera puede agregar
|
|
17
|
+
// uno propio—. Los hooks sí se quedan en el proyecto: la configuración del runner los nombra por
|
|
18
|
+
// ruta literal y no sabe resolver en cascada.
|
|
19
|
+
// Se busca por `runners/` y no por `automatization/`: toda instancia tiene el segundo —ahí viven sus
|
|
20
|
+
// hooks— y encontrarlo daría por buena una dependencia sin instalar.
|
|
21
|
+
function packagedAutomation(root) {
|
|
22
|
+
const runners = O.packagePath(root, path.join('automatization', 'runners'))
|
|
23
|
+
return runners ? path.dirname(runners) : ''
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
function runnerManifest(root, name) {
|
|
15
27
|
if (!RUNNER_NAMES.includes(name)) {
|
|
16
28
|
throw new Error(`runner debe ser ${RUNNER_NAMES.join(', ')}`)
|
|
17
29
|
}
|
|
18
|
-
const
|
|
30
|
+
const packaged = packagedAutomation(root)
|
|
31
|
+
if (!packaged) {
|
|
32
|
+
throw new Error('no encuentro automatization/: instalá la dependencia o restaurá .ops/')
|
|
33
|
+
}
|
|
34
|
+
const file = path.join(packaged, 'runners', name, 'manifest.json')
|
|
19
35
|
try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch (error) {
|
|
20
36
|
throw new Error(`${name}: manifest inválido (${error.message})`)
|
|
21
37
|
}
|
|
@@ -57,20 +73,38 @@ function includesConfig(actual, expected) {
|
|
|
57
73
|
return actual === expected
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
// Dónde abre el dev su herramienta, que no siempre es la raíz ops. En modo sidecar el repo ops es
|
|
77
|
+
// un hermano de los repos de producto: `aparatejo-ops/` coordina, `aparatejo/` es lo que se abre.
|
|
78
|
+
// Instalar dentro del sidecar dejaría al runner sin ver una sola línea de código.
|
|
79
|
+
function installRoot(root) {
|
|
80
|
+
try {
|
|
81
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
82
|
+
if (config.mode === 'sidecar') return path.resolve(root, '..')
|
|
83
|
+
} catch { /* sin configuración legible, instalar donde está */ }
|
|
84
|
+
return root
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Cómo se nombra la raíz ops desde ahí: `aparatejo-ops/` en sidecar, vacío cuando coinciden.
|
|
88
|
+
function opsPrefix(root) {
|
|
89
|
+
const relative = path.relative(installRoot(root), root)
|
|
90
|
+
return relative ? `${relative.split(path.sep).join('/')}/` : ''
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
function runnerPaths(root, name, runner) {
|
|
61
|
-
const automationRoot =
|
|
62
|
-
const sourceDir = path.join(
|
|
94
|
+
const automationRoot = packagedAutomation(root)
|
|
95
|
+
const sourceDir = path.join(automationRoot, 'runners', name)
|
|
96
|
+
const install = installRoot(root)
|
|
63
97
|
const configSource = F.assertWithin(
|
|
64
98
|
sourceDir,
|
|
65
99
|
path.resolve(sourceDir, runner.config.source),
|
|
66
100
|
`${name}: config.source`,
|
|
67
101
|
)
|
|
68
102
|
const configTarget = F.assertWithin(
|
|
69
|
-
|
|
70
|
-
path.resolve(
|
|
103
|
+
install,
|
|
104
|
+
path.resolve(install, runner.config.target),
|
|
71
105
|
`${name}: config.target`,
|
|
72
106
|
)
|
|
73
|
-
return { automationRoot, sourceDir, configSource, configTarget }
|
|
107
|
+
return { automationRoot, sourceDir, configSource, configTarget, install }
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
function resolveItem(paths, root, name, item) {
|
|
@@ -80,16 +114,30 @@ function resolveItem(paths, root, name, item) {
|
|
|
80
114
|
path.resolve(paths.sourceDir, item.source),
|
|
81
115
|
`${name}: source`,
|
|
82
116
|
),
|
|
83
|
-
target: F.assertWithin(
|
|
117
|
+
target: F.assertWithin(
|
|
118
|
+
paths.install,
|
|
119
|
+
path.resolve(paths.install, item.target),
|
|
120
|
+
`${name}: target`,
|
|
121
|
+
),
|
|
84
122
|
}
|
|
85
123
|
}
|
|
86
124
|
|
|
125
|
+
// La configuración del runner nombra cada guard por ruta relativa a donde se abre la herramienta.
|
|
126
|
+
// Si eso ya no es la raíz ops, hay que reescribir el prefijo o el runner buscaría los guards en la
|
|
127
|
+
// carpeta equivocada. Se hace en un solo lugar: `install` lo escribe y `doctor` compara contra esto.
|
|
128
|
+
function runnerConfig(paths, root) {
|
|
129
|
+
const raw = fs.readFileSync(paths.configSource, 'utf8')
|
|
130
|
+
const prefix = opsPrefix(root)
|
|
131
|
+
return JSON.parse(prefix ? raw.split('automatization/hooks/').join(`${prefix}automatization/hooks/`) : raw)
|
|
132
|
+
}
|
|
133
|
+
|
|
87
134
|
// Cargos del catálogo, con el frontmatter que el runner indexa para elegir a quién invocar.
|
|
88
135
|
function roleCatalog(root) {
|
|
89
136
|
return catalog.list(root)
|
|
90
137
|
.map((role) => {
|
|
91
138
|
const field = P.frontmatter(fs.readFileSync(path.join(role.dir, 'SKILL.md'), 'utf8'))
|
|
92
|
-
|
|
139
|
+
const reference = path.relative(installRoot(root), role.dir).split(path.sep).join('/')
|
|
140
|
+
return { ...role, reference, description: field('description') }
|
|
93
141
|
})
|
|
94
142
|
.filter((role) => role.description)
|
|
95
143
|
}
|
|
@@ -115,11 +163,12 @@ Respetá los límites de ese contrato y las reglas de \`AGENTS.md\`. Generado po
|
|
|
115
163
|
|
|
116
164
|
function installRoleSkills(root, runner, output) {
|
|
117
165
|
if (!runner.capabilities.nativeSkills || !runner.roleSkills) return 0
|
|
118
|
-
const
|
|
166
|
+
const install = installRoot(root)
|
|
167
|
+
const base = F.assertWithin(install, path.resolve(install, runner.roleSkills), `${runner.name}: roleSkills`)
|
|
119
168
|
const roles = roleCatalog(root)
|
|
120
169
|
for (const role of roles) {
|
|
121
170
|
const file = path.join(base, role.slug, 'SKILL.md')
|
|
122
|
-
F.assertNoSymlinkPath(
|
|
171
|
+
F.assertNoSymlinkPath(install, file)
|
|
123
172
|
F.atomicWrite(file, roleSkill(role))
|
|
124
173
|
}
|
|
125
174
|
if (roles.length) output.log(`✓ ${runner.name}: ${roles.length} cargo(s) disponibles en ${runner.roleSkills}`)
|
|
@@ -141,6 +190,30 @@ function hasHooks(config) {
|
|
|
141
190
|
})
|
|
142
191
|
}
|
|
143
192
|
|
|
193
|
+
// Guards de la instancia que ya no coinciden con los del paquete. Existir y ser ejecutable no
|
|
194
|
+
// alcanza: un guard viejo no falla, deja de proteger sin decir nada. La instancia declaraba una
|
|
195
|
+
// versión y nadie comprobaba que su runtime fuera realmente esa.
|
|
196
|
+
function staleHooks(root) {
|
|
197
|
+
const packaged = packagedAutomation(root)
|
|
198
|
+
if (!packaged) return []
|
|
199
|
+
const shipped = path.join(packaged, 'hooks')
|
|
200
|
+
const mine = path.join(root, 'automatization', 'hooks')
|
|
201
|
+
const recorded = M.read(root)
|
|
202
|
+
const stale = []
|
|
203
|
+
let names = []
|
|
204
|
+
try { names = fs.readdirSync(shipped) } catch { return [] }
|
|
205
|
+
for (const file of names) {
|
|
206
|
+
const local = path.join(mine, file)
|
|
207
|
+
// Los que faltan ya los reporta el chequeo de arriba; acá sólo interesa el que quedó atrás.
|
|
208
|
+
if (!fs.existsSync(local)) continue
|
|
209
|
+
const current = M.digest(local)
|
|
210
|
+
if (current === M.digest(path.join(shipped, file))) continue
|
|
211
|
+
const delivered = recorded[`automatization/hooks/${file}`]
|
|
212
|
+
stale.push({ file, edited: Boolean(delivered) && delivered !== current })
|
|
213
|
+
}
|
|
214
|
+
return stale
|
|
215
|
+
}
|
|
216
|
+
|
|
144
217
|
function check(root) {
|
|
145
218
|
const errors = []
|
|
146
219
|
const hookDir = path.join(root, 'automatization', 'hooks')
|
|
@@ -176,11 +249,19 @@ function check(root) {
|
|
|
176
249
|
}
|
|
177
250
|
const workflows = ['autobuild.js', 'team.js', path.join('integrations', 'sync.js')]
|
|
178
251
|
workflows.push(path.join('integrations', 'promote.js'))
|
|
252
|
+
const packaged = packagedAutomation(root)
|
|
179
253
|
for (const name of workflows) {
|
|
180
|
-
if (!fs.existsSync(path.join(
|
|
181
|
-
errors.push(`falta automatization/workflows/${name}
|
|
254
|
+
if (!packaged || !fs.existsSync(path.join(packaged, 'workflows', name))) {
|
|
255
|
+
errors.push(`falta automatization/workflows/${name}: instalá la dependencia o restaurá .ops/`)
|
|
182
256
|
}
|
|
183
257
|
}
|
|
258
|
+
for (const { file, edited } of staleHooks(root)) {
|
|
259
|
+
errors.push(edited
|
|
260
|
+
? `automatization/hooks/${file}: lo editaste y es del toolkit; agregá un guard propio al lado `
|
|
261
|
+
+ 'o descartá tu cambio con `cauce upgrade --force`'
|
|
262
|
+
: `automatization/hooks/${file}: quedó atrás del paquete y ya no protege lo que dice; `
|
|
263
|
+
+ 'corré `cauce upgrade` antes de instalar el runner')
|
|
264
|
+
}
|
|
184
265
|
for (const name of RUNNER_NAMES) validateRunner(root, name, errors)
|
|
185
266
|
return errors
|
|
186
267
|
}
|
|
@@ -194,7 +275,7 @@ function validateRunner(root, name, errors) {
|
|
|
194
275
|
return
|
|
195
276
|
}
|
|
196
277
|
const paths = runnerPaths(root, name, runner)
|
|
197
|
-
const config =
|
|
278
|
+
const config = runnerConfig(paths, root)
|
|
198
279
|
if (runner.capabilities.nativeHooks && !hasHooks(config)) {
|
|
199
280
|
errors.push(`${name}: declara hooks nativos pero no los configura`)
|
|
200
281
|
}
|
|
@@ -272,7 +353,7 @@ function doctor(root, name, output = console) {
|
|
|
272
353
|
const errors = []
|
|
273
354
|
const warnings = []
|
|
274
355
|
try {
|
|
275
|
-
const expected =
|
|
356
|
+
const expected = runnerConfig(paths, root)
|
|
276
357
|
const actual = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
277
358
|
if (!includesConfig(actual, expected)) {
|
|
278
359
|
errors.push(`${runner.config.target}: configuración instalada incompleta o divergente`)
|
|
@@ -286,18 +367,27 @@ function doctor(root, name, output = console) {
|
|
|
286
367
|
errors.push(`${runner.config.target}: ${error.message}`)
|
|
287
368
|
}
|
|
288
369
|
for (const item of runner.instructions || []) {
|
|
289
|
-
const resolved = resolveItem(paths, root, name, item)
|
|
370
|
+
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
290
371
|
if (!fs.existsSync(resolved.target)) errors.push(`falta ${item.target}`)
|
|
291
372
|
else if (!fs.readFileSync(resolved.target, 'utf8').includes('AGENTS.md')) {
|
|
292
373
|
warnings.push(`${item.target}: no referencia AGENTS.md; verifica las reglas globales`)
|
|
293
374
|
}
|
|
375
|
+
if (deliveryState(M.readRunners(root), name, resolved) === 'desactualizado') {
|
|
376
|
+
warnings.push(`${item.target}: Cauce trae una versión más nueva y vos no lo tocaste; reinstalá`)
|
|
377
|
+
}
|
|
294
378
|
}
|
|
379
|
+
// Divergir no es un error: puede ser una mejora río arriba esperando reinstalación. Reportarlo
|
|
380
|
+
// como error dejaba a la empresa sin salida, porque `install` tampoco lo actualizaba.
|
|
381
|
+
const recorded = M.readRunners(root)
|
|
295
382
|
for (const item of runner.artifacts || []) {
|
|
296
|
-
const resolved = resolveItem(paths, root, name, item)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
383
|
+
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
384
|
+
const situacion = deliveryState(recorded, name, resolved)
|
|
385
|
+
if (situacion === 'nuevo') errors.push(`falta ${item.target}`)
|
|
386
|
+
else if (situacion === 'desactualizado') {
|
|
387
|
+
warnings.push(`${item.target}: hay una versión más nueva en Cauce; reinstalá el adaptador`)
|
|
388
|
+
} else if (situacion === 'ajeno') {
|
|
389
|
+
warnings.push(`${item.target}: lo editaste y es del toolkit; `
|
|
390
|
+
+ 'agregá lo tuyo al lado o reinstalá con --force para volver a la versión de Cauce')
|
|
301
391
|
}
|
|
302
392
|
}
|
|
303
393
|
// Un cargo que quedó fuera, o cuya descripción cambió en el catálogo, deja al runner eligiendo
|
|
@@ -306,7 +396,7 @@ function doctor(root, name, output = console) {
|
|
|
306
396
|
const missing = []
|
|
307
397
|
const stale = []
|
|
308
398
|
for (const role of roleCatalog(root)) {
|
|
309
|
-
const file = path.join(
|
|
399
|
+
const file = path.join(paths.install, runner.roleSkills, role.slug, 'SKILL.md')
|
|
310
400
|
if (!fs.existsSync(file)) missing.push(role.slug)
|
|
311
401
|
else if (fs.readFileSync(file, 'utf8') !== roleSkill(role)) stale.push(role.slug)
|
|
312
402
|
}
|
|
@@ -327,12 +417,35 @@ function doctor(root, name, output = console) {
|
|
|
327
417
|
return { runner, errors, warnings }
|
|
328
418
|
}
|
|
329
419
|
|
|
330
|
-
|
|
420
|
+
// Clave de entrega de un archivo del adaptador. Va en su propia sección del manifiesto porque puede
|
|
421
|
+
// caer fuera de la instancia: en sidecar el wiring vive en la carpeta de la compañía.
|
|
422
|
+
function deliveryKey(name, target) {
|
|
423
|
+
return `${name}/${target.split(path.sep).join('/')}`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// En qué estado quedó un archivo que este adaptador entregó alguna vez.
|
|
427
|
+
//
|
|
428
|
+
// nuevo no existe todavía
|
|
429
|
+
// al día idéntico a lo que trae Cauce
|
|
430
|
+
// desactualizado la empresa no lo tocó, pero río arriba cambió
|
|
431
|
+
// ajeno difiere de lo entregado: alguien lo editó acá
|
|
432
|
+
//
|
|
433
|
+
// Sin el registro de entrega, `desactualizado` y `ajeno` se ven igual. `install` resolvía esa duda
|
|
434
|
+
// conservando siempre, así que ninguna mejora del toolkit llegaba nunca a un runner ya instalado.
|
|
435
|
+
function deliveryState(recorded, name, resolved) {
|
|
436
|
+
if (!fs.existsSync(resolved.target)) return 'nuevo'
|
|
437
|
+
const current = M.digest(resolved.target)
|
|
438
|
+
if (current === M.digest(resolved.source)) return 'al día'
|
|
439
|
+
const delivered = recorded[deliveryKey(name, resolved.item.target)]
|
|
440
|
+
return delivered && delivered === current ? 'desactualizado' : 'ajeno'
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function install(root, name, output = console, options = {}) {
|
|
331
444
|
const runner = runnerManifest(root, name)
|
|
332
445
|
const errors = check(root)
|
|
333
446
|
if (errors.length) throw new Error(`La automatización no es instalable:\n- ${errors.join('\n- ')}`)
|
|
334
447
|
const paths = runnerPaths(root, name, runner)
|
|
335
|
-
const incoming =
|
|
448
|
+
const incoming = runnerConfig(paths, root)
|
|
336
449
|
let current = {}
|
|
337
450
|
if (fs.existsSync(paths.configTarget)) {
|
|
338
451
|
try { current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8')) } catch (error) {
|
|
@@ -341,31 +454,57 @@ function install(root, name, output = console) {
|
|
|
341
454
|
}
|
|
342
455
|
const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
|
|
343
456
|
const resolvedItems = items.map((item) => ({ item, ...resolveItem(paths, root, name, item) }))
|
|
344
|
-
F.assertNoSymlinkPath(
|
|
345
|
-
for (const resolved of resolvedItems)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
457
|
+
F.assertNoSymlinkPath(paths.install, paths.configTarget)
|
|
458
|
+
for (const resolved of resolvedItems) F.assertNoSymlinkPath(paths.install, resolved.target)
|
|
459
|
+
|
|
460
|
+
const recorded = M.readRunners(root)
|
|
461
|
+
const state = new Map(resolvedItems.map((resolved) => [resolved, deliveryState(recorded, name, resolved)]))
|
|
462
|
+
// Un archivo de instrucciones es del proyecto y se conserva; uno ejecutable es del toolkit y se
|
|
463
|
+
// reemplaza. Editarlo detiene la instalación antes de pisarlo, igual que hace `upgrade`.
|
|
464
|
+
const editados = resolvedItems.filter((resolved) => {
|
|
465
|
+
return state.get(resolved) === 'ajeno' && !runner.instructions.includes(resolved.item)
|
|
466
|
+
})
|
|
467
|
+
if (editados.length && !options.force) {
|
|
468
|
+
throw new Error(
|
|
469
|
+
`${editados.length} archivo(s) que mantiene Cauce fueron editados y se perderían:\n`
|
|
470
|
+
+ `${editados.map((resolved) => `- ${resolved.item.target}`).join('\n')}\n\n`
|
|
471
|
+
+ 'Son del toolkit: en vez de editarlos, agregá lo tuyo al lado y registralo en la\n'
|
|
472
|
+
+ 'configuración de tu runner. Si el cambio ya no te sirve, repetí con --force.',
|
|
473
|
+
)
|
|
352
474
|
}
|
|
353
475
|
const merged = pruneSupersededHooks(mergeConfig(current, incoming))
|
|
354
476
|
F.atomicWriteJson(paths.configTarget, merged.config)
|
|
355
477
|
for (const { wrapper, files } of merged.replaced) {
|
|
356
478
|
output.log(`− ${name}: reemplazado ${[...new Set(files)].join(', ')} por ${wrapper}`)
|
|
357
479
|
}
|
|
480
|
+
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
481
|
+
// el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
|
|
482
|
+
if (paths.install !== root) {
|
|
483
|
+
output.log(` ${name}: el runner se abre en ${paths.install} — ahí quedan .claude/ y CLAUDE.md`)
|
|
484
|
+
}
|
|
358
485
|
output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
|
|
486
|
+
const entregado = { ...recorded }
|
|
359
487
|
for (const resolved of resolvedItems) {
|
|
360
|
-
|
|
361
|
-
|
|
488
|
+
const situacion = state.get(resolved)
|
|
489
|
+
const propio = runner.instructions.includes(resolved.item)
|
|
490
|
+
if (situacion === 'ajeno' && propio) {
|
|
491
|
+
output.log(`= ${name}: conservado ${resolved.item.target} (tiene cambios tuyos)`)
|
|
492
|
+
} else if (situacion === 'al día') {
|
|
493
|
+
output.log(`= ${name}: ${resolved.item.target} ya está al día`)
|
|
362
494
|
} else {
|
|
363
495
|
fs.mkdirSync(path.dirname(resolved.target), { recursive: true })
|
|
364
496
|
fs.copyFileSync(resolved.source, resolved.target)
|
|
365
|
-
|
|
497
|
+
const verbo = situacion === 'nuevo' ? 'instalado' : 'actualizado'
|
|
498
|
+
output.log(`✓ ${name}: ${verbo} ${resolved.item.target}`)
|
|
499
|
+
}
|
|
500
|
+
// Sólo se anota lo que Cauce puso: un archivo conservado con cambios de la empresa no es una
|
|
501
|
+
// entrega, y registrarlo lo volvería indistinguible de uno intacto en la próxima instalación.
|
|
502
|
+
if (!(situacion === 'ajeno' && propio)) {
|
|
503
|
+
entregado[deliveryKey(name, resolved.item.target)] = M.digest(resolved.target)
|
|
366
504
|
}
|
|
367
505
|
}
|
|
368
506
|
installRoleSkills(root, runner, output)
|
|
507
|
+
M.write(root, undefined, entregado)
|
|
369
508
|
return runner
|
|
370
509
|
}
|
|
371
510
|
|
package/engine/cli/ops.js
CHANGED
|
@@ -140,20 +140,6 @@ function init(target) {
|
|
|
140
140
|
preserve,
|
|
141
141
|
root,
|
|
142
142
|
)
|
|
143
|
-
copyRuntime(
|
|
144
|
-
path.join(PROJECT_ROOT, 'automatization', 'runners'),
|
|
145
|
-
path.join(root, 'automatization', 'runners'),
|
|
146
|
-
preserve,
|
|
147
|
-
root,
|
|
148
|
-
// El README de runners lo provee el template: le habla al proyecto, no a quien desarrolla Cauce.
|
|
149
|
-
O.TEMPLATE_OWNED.filter((owned) => owned.startsWith('automatization/runners/')).map((owned) => path.basename(owned)),
|
|
150
|
-
)
|
|
151
|
-
copyRuntime(
|
|
152
|
-
path.join(PROJECT_ROOT, 'automatization', 'workflows'),
|
|
153
|
-
path.join(root, 'automatization', 'workflows'),
|
|
154
|
-
preserve,
|
|
155
|
-
root,
|
|
156
|
-
)
|
|
157
143
|
const version = require(path.join(PROJECT_ROOT, 'package.json')).version
|
|
158
144
|
// Un repo con npm recibe el motor como dependencia versionada; uno de Go, Python o Rust recibe
|
|
159
145
|
// la copia, porque exigirle un package.json para correr su planning sería imponerle un stack.
|
|
@@ -165,9 +151,18 @@ function init(target) {
|
|
|
165
151
|
const engine = path.join(root, '.ops', 'engine')
|
|
166
152
|
copyRuntime(path.join(PROJECT_ROOT, 'engine'), engine, preserve, root)
|
|
167
153
|
fs.chmodSync(path.join(engine, 'cli', 'ops.js'), 0o755)
|
|
168
|
-
// Sin npm no hay de dónde leer el catálogo en tiempo de ejecución: viaja junto al motor
|
|
154
|
+
// Sin npm no hay de dónde leer el catálogo en tiempo de ejecución: viaja junto al motor, igual
|
|
155
|
+
// que los adaptadores de runner y los workflows que instala `automation install`.
|
|
169
156
|
copyRuntime(path.join(PROJECT_ROOT, 'agents'), path.join(root, '.ops', 'agents'), preserve, root)
|
|
170
157
|
copyRuntime(path.join(PROJECT_ROOT, 'teams'), path.join(root, '.ops', 'teams'), preserve, root)
|
|
158
|
+
for (const name of ['runners', 'workflows']) {
|
|
159
|
+
copyRuntime(
|
|
160
|
+
path.join(PROJECT_ROOT, 'automatization', name),
|
|
161
|
+
path.join(root, '.ops', 'automatization', name),
|
|
162
|
+
preserve,
|
|
163
|
+
root,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
171
166
|
}
|
|
172
167
|
let entregado = {}
|
|
173
168
|
for (const relative of O.trackedPaths()) {
|
|
@@ -546,10 +541,7 @@ function upgrade(dir) {
|
|
|
546
541
|
// `.ops/` es el paquete vendorizado de una instancia sin npm. Si no existe, esta instancia lo
|
|
547
542
|
// toma de la dependencia y crearlo sería duplicar lo que el lockfile ya versiona.
|
|
548
543
|
if (relative.startsWith('.ops/') && !fs.existsSync(target)) continue
|
|
549
|
-
|
|
550
|
-
.filter((owned) => owned.startsWith(`${relative}/`))
|
|
551
|
-
.map((owned) => path.basename(owned))
|
|
552
|
-
if (fs.statSync(origin).isDirectory()) overlayTree(origin, target, root, skip)
|
|
544
|
+
if (fs.statSync(origin).isDirectory()) overlayTree(origin, target, root)
|
|
553
545
|
else {
|
|
554
546
|
F.assertNoSymlinkPath(root, target)
|
|
555
547
|
F.atomicWrite(target, fs.readFileSync(origin, 'utf8'))
|
|
@@ -573,7 +565,7 @@ function upgrade(dir) {
|
|
|
573
565
|
const dir = path.join(root, relative)
|
|
574
566
|
if (fs.existsSync(dir)) registro = { ...registro, ...M.record(root, relative, O.treeFiles(dir)) }
|
|
575
567
|
}
|
|
576
|
-
M.write(root, registro)
|
|
568
|
+
M.write(root, M.prune(root, registro))
|
|
577
569
|
|
|
578
570
|
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
579
571
|
config.cauceVersion = to
|
|
@@ -590,6 +582,14 @@ function upgrade(dir) {
|
|
|
590
582
|
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
591
583
|
}
|
|
592
584
|
console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
585
|
+
// El wiring del runner no se actualiza solo: vive fuera de la instancia y lo escribe otro comando.
|
|
586
|
+
// Sin este recordatorio, una mejora en un workflow o en el catálogo se queda en el paquete.
|
|
587
|
+
const runners = Object.keys(M.readRunners(root))
|
|
588
|
+
.map((key) => key.split('/')[0])
|
|
589
|
+
.filter((name, index, all) => all.indexOf(name) === index)
|
|
590
|
+
for (const name of runners) {
|
|
591
|
+
console.log(` reinstalá tu runner para que el wiring quede al día: make install-${name}`)
|
|
592
|
+
}
|
|
593
593
|
}
|
|
594
594
|
|
|
595
595
|
// Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
|
|
@@ -717,7 +717,8 @@ function automation(action, rootArg, runnerName) {
|
|
|
717
717
|
}
|
|
718
718
|
if (action === 'install') {
|
|
719
719
|
let runner
|
|
720
|
-
|
|
720
|
+
const force = process.argv.includes('--force')
|
|
721
|
+
try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
|
|
721
722
|
if (runnerName === 'codex') {
|
|
722
723
|
console.log(' Codex pedirá revisar y confiar en hooks nuevos al iniciar sesión.')
|
|
723
724
|
}
|
package/engine/core/manifest.js
CHANGED
|
@@ -19,18 +19,38 @@ function digest(file) {
|
|
|
19
19
|
} catch { return '' }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Dos secciones porque son dos entregas distintas: `files` es lo que se materializó dentro de la
|
|
23
|
+
// instancia y `runners` lo que un adaptador dejó fuera de ella —en modo sidecar el wiring vive en
|
|
24
|
+
// la carpeta de la compañía—. El registro se queda igual acá, que es el repo que la empresa versiona.
|
|
25
|
+
function readAll(root) {
|
|
23
26
|
try {
|
|
24
|
-
const data = JSON.parse(fs.readFileSync(path.join(root, FILE), 'utf8'))
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
+
const data = JSON.parse(fs.readFileSync(path.join(root, FILE), 'utf8')) || {}
|
|
28
|
+
return {
|
|
29
|
+
files: typeof data.files === 'object' && data.files ? data.files : {},
|
|
30
|
+
runners: typeof data.runners === 'object' && data.runners ? data.runners : {},
|
|
31
|
+
}
|
|
32
|
+
} catch { return { files: {}, runners: {} } }
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
function
|
|
35
|
+
function read(root) { return readAll(root).files }
|
|
36
|
+
|
|
37
|
+
function readRunners(root) { return readAll(root).runners }
|
|
38
|
+
|
|
39
|
+
// Cada sección que no se pasa se conserva: `upgrade` no sabe del wiring y `install` no sabe de la
|
|
40
|
+
// instancia, y ninguno de los dos debería borrar lo que el otro anotó.
|
|
41
|
+
function write(root, files, runners) {
|
|
42
|
+
const current = readAll(root)
|
|
30
43
|
const target = path.join(root, FILE)
|
|
31
44
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
32
|
-
const ordered =
|
|
33
|
-
|
|
45
|
+
const ordered = (map) => Object.fromEntries(
|
|
46
|
+
Object.entries(map).sort(([left], [right]) => left.localeCompare(right)),
|
|
47
|
+
)
|
|
48
|
+
const data = {
|
|
49
|
+
version: 1,
|
|
50
|
+
files: ordered(files || current.files),
|
|
51
|
+
runners: ordered(runners || current.runners),
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(target, `${JSON.stringify(data, null, 2)}\n`)
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
// Registra lo entregado en una ruta, relativo a la raíz de la instancia.
|
|
@@ -40,6 +60,15 @@ function record(root, relative, files) {
|
|
|
40
60
|
return current
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
// Olvida lo que ya no está en disco. Sin esto el registro sólo crece: una ruta retirada deja su
|
|
64
|
+
// digest para siempre, y el día que un nombre se reutilice la entrega nueva se leería como una
|
|
65
|
+
// edición local y detendría la actualización.
|
|
66
|
+
function prune(root, files) {
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
Object.entries(files).filter(([file]) => fs.existsSync(path.join(root, file))),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
// Archivos que la empresa modificó después de recibirlos. Un archivo sin registro previo no
|
|
44
73
|
// cuenta: llegó con una versión anterior a este mecanismo, o lo agregó el proyecto.
|
|
45
74
|
function edited(root, relative, files) {
|
|
@@ -51,4 +80,4 @@ function edited(root, relative, files) {
|
|
|
51
80
|
})
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
module.exports = { FILE, digest, edited, read, record, write }
|
|
83
|
+
module.exports = { FILE, digest, edited, prune, read, readRunners, record, write }
|
package/engine/core/ownership.js
CHANGED
|
@@ -23,6 +23,10 @@ const SYSTEM_FILES = [
|
|
|
23
23
|
'planning/roadmap/README.md',
|
|
24
24
|
'planning/roadmap/epic-000-template.md',
|
|
25
25
|
'organization/roles/README.md',
|
|
26
|
+
'teams/000-template.md',
|
|
27
|
+
'teams/README.md',
|
|
28
|
+
'automatization/README.md',
|
|
29
|
+
'automatization/AGENTS.md',
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
// Colecciones mixtas: el toolkit posee `<dir>/system/`, el proyecto todo lo demás del directorio.
|
|
@@ -39,23 +43,27 @@ const RUNTIME_PATHS = [
|
|
|
39
43
|
'.ops/engine',
|
|
40
44
|
'.ops/agents',
|
|
41
45
|
'.ops/teams',
|
|
46
|
+
'.ops/automatization/runners',
|
|
47
|
+
'.ops/automatization/workflows',
|
|
42
48
|
'automatization/hooks',
|
|
43
|
-
'automatization/runners',
|
|
44
|
-
'automatization/workflows',
|
|
45
49
|
]
|
|
46
50
|
|
|
47
|
-
// Rutas que el paquete tiene por duplicado: una versión para el proyecto en `template/` y otra
|
|
48
|
-
// interna del toolkit. Gana la del template, que es la que le habla a quien usa la instancia.
|
|
49
|
-
const TEMPLATE_OWNED = ['automatization/runners/README.md']
|
|
50
|
-
|
|
51
51
|
// Dentro del paquete, lo que una instancia recibe en su raíz vive bajo `template/`; el catálogo,
|
|
52
52
|
// los equipos y la automatización están en la raíz del paquete, y el motor en `engine/`.
|
|
53
|
-
const TEMPLATE_PREFIXES = [
|
|
53
|
+
const TEMPLATE_PREFIXES = [
|
|
54
|
+
'planning/',
|
|
55
|
+
'organization/',
|
|
56
|
+
'integrations/',
|
|
57
|
+
'teams/',
|
|
58
|
+
'automatization/',
|
|
59
|
+
]
|
|
54
60
|
|
|
55
61
|
function sourceOf(relative) {
|
|
56
|
-
|
|
57
|
-
if (relative
|
|
58
|
-
|
|
62
|
+
// `.ops/` es el paquete vendorizado: cada ruta de ahí adentro se llama igual en el origen.
|
|
63
|
+
if (relative.startsWith('.ops/')) return relative.slice('.ops/'.length)
|
|
64
|
+
// El runtime sale de la raíz del paquete aunque su prefijo sea de plantilla: `automatization/`
|
|
65
|
+
// le entrega documentos a la instancia, pero los guards que ejecuta son los del toolkit.
|
|
66
|
+
if (RUNTIME_PATHS.includes(relative)) return relative
|
|
59
67
|
if (TEMPLATE_PREFIXES.some((prefix) => relative.startsWith(prefix))) {
|
|
60
68
|
return path.join('template', relative)
|
|
61
69
|
}
|
|
@@ -79,6 +87,17 @@ function engineAt(root, relative = '') {
|
|
|
79
87
|
.find((candidate) => fs.existsSync(candidate)) || ''
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
// Una ruta cualquiera del paquete, en el mismo orden de preferencia que el motor. La usan los
|
|
91
|
+
// adaptadores de runner y los workflows: el motor los consume, el proyecto no los materializa.
|
|
92
|
+
function packagePath(root, relative) {
|
|
93
|
+
const candidates = [
|
|
94
|
+
path.join(root, 'node_modules', '@ingeniomaps', 'cauce', relative),
|
|
95
|
+
path.join(root, '.ops', relative),
|
|
96
|
+
path.join(root, relative),
|
|
97
|
+
]
|
|
98
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || ''
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
// Definiciones que consume el motor —cargos y equipos— y que por eso viajan con el paquete en vez
|
|
83
102
|
// de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
|
|
84
103
|
// proyecto deba crear. Una sola implementación para las dos, o divergen.
|
|
@@ -133,6 +152,8 @@ const RETIRED = [
|
|
|
133
152
|
'agents/roles/system',
|
|
134
153
|
'teams/system',
|
|
135
154
|
'.github/workflows/agent-learning.yml',
|
|
155
|
+
'automatization/runners',
|
|
156
|
+
'automatization/workflows',
|
|
136
157
|
]
|
|
137
158
|
|
|
138
159
|
// Aprendizaje que quedó dentro de una ruta retirada. Es lo único ahí que no se puede reponer, así
|
|
@@ -203,7 +224,7 @@ module.exports = {
|
|
|
203
224
|
retiredWithLearning,
|
|
204
225
|
engineAt,
|
|
205
226
|
engineCandidates,
|
|
206
|
-
|
|
227
|
+
packagePath,
|
|
207
228
|
SYSTEM_COLLECTIONS,
|
|
208
229
|
SYSTEM_FILES,
|
|
209
230
|
identity,
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -58,8 +58,9 @@ Aplica a `planning/business-rules/`, `planning/adr/`, `planning/rules/`, `teams/
|
|
|
58
58
|
`planning/` —roadmap, backlog, WIP, done, inbox y acciones humanas— es del proyecto y no se toca al
|
|
59
59
|
actualizar.
|
|
60
60
|
|
|
61
|
-
`automatization/hooks/`
|
|
62
|
-
|
|
61
|
+
`automatization/hooks/` es runtime del toolkit y se reemplaza entero. No tiene `system/` porque no hace
|
|
62
|
+
falta: lo que un proyecto necesita ya funciona sin editarlo. Los adaptadores de runner y los workflows ni
|
|
63
|
+
siquiera están acá — los lee el motor desde el paquete.
|
|
63
64
|
|
|
64
65
|
- **Agregar un guard propio**: creá `automatization/hooks/guard-<nombre>.sh` y registralo en la
|
|
65
66
|
configuración de tu runner. Sobrevive a cada actualización, porque el toolkit no lo conoce.
|
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
# Automatización
|
|
1
|
+
# Automatización
|
|
2
2
|
|
|
3
3
|
Wiring local entre `planning/PROTOCOL.md` y el runner elegido. El proyecto empieza en modo manual y seguro;
|
|
4
|
-
|
|
4
|
+
ningún runner se activa solo.
|
|
5
5
|
|
|
6
6
|
- `config.json`: runner activo y gates requeridos.
|
|
7
|
-
- `hooks/`:
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- `hooks/`: los guards que se ejecutan. Acá va también el tuyo: agregalo con otro nombre y registralo
|
|
8
|
+
en la configuración de tu runner, que es del proyecto y sobrevive a cada actualización.
|
|
9
|
+
|
|
10
|
+
Los adaptadores de runner y los workflows no se copian acá: son definiciones que el motor consume y
|
|
11
|
+
viajan con Cauce, igual que el catálogo de cargos y los equipos. `automation install` los lee desde ahí.
|
|
10
12
|
|
|
11
13
|
No copies reglas del protocolo aquí: enlázalas y mecaniza únicamente lo comprobable.
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
node tools/ops.js automation check .
|
|
15
|
-
node tools/ops.js automation install .
|
|
17
|
+
node tools/ops.js automation install . claude
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Dónde aterriza**: en modo `embedded`, acá mismo. En modo `sidecar` el runner se instala en la carpeta
|
|
21
|
+
de la compañía —la que contiene este repo y los de producto—, porque es donde el dev abre la
|
|
22
|
+
herramienta y la única desde la que ve el código. El comando dice la ruta exacta al terminar.
|
|
23
|
+
|
|
24
|
+
Hay adaptadores para Claude, Codex, Antigravity y Gemini. Antigravity (`agy`) es la opción Google
|
|
25
|
+
recomendada para cuentas individuales y proyectos nuevos; Gemini se conserva para Enterprise, Google
|
|
26
|
+
Cloud y API keys. La instalación conserva la configuración existente, y `doctor` comprueba archivos y
|
|
27
|
+
disponibilidad del CLI sin autenticarse.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
make install-antigravity
|
|
31
|
+
make doctor-antigravity
|
|
16
32
|
```
|
|
17
33
|
|
|
18
|
-
También
|
|
19
|
-
Valida después con `make doctor-claude`, `make doctor-codex` o `make doctor-gemini`.
|
|
34
|
+
También existen `make install-claude`, `make install-codex` y `make install-gemini`, con sus `doctor-*`.
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Runners
|
|
2
|
-
|
|
3
|
-
Esta instancia incluye adaptadores instalables para Claude, Codex, Antigravity y Gemini, pero ninguno se
|
|
4
|
-
activa automáticamente. Antigravity (`agy`) es la opción Google recomendada para cuentas individuales y
|
|
5
|
-
proyectos nuevos. Gemini se conserva para Enterprise, Google Cloud y API keys.
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
make install-antigravity
|
|
9
|
-
make doctor-antigravity
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Usa el alias equivalente para otro runner. La instalación conserva configuración existente y `doctor`
|
|
13
|
-
comprueba archivos y disponibilidad del CLI sin autenticarse.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|