@ingeniomaps/cauce 0.4.1 → 0.5.1
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 +67 -0
- package/README.md +5 -4
- package/automatization/hooks/run-hook.sh +4 -0
- package/automatization/runners/antigravity/rules/cauce.md +2 -2
- package/automatization/runners/claude/CLAUDE.md +4 -4
- package/automatization/runners/claude/settings.json +3 -3
- package/automatization/runners/codex/AGENTS.md +47 -0
- package/automatization/runners/codex/hooks.json +3 -3
- package/automatization/runners/codex/manifest.json +6 -1
- package/automatization/runners/gemini/GEMINI.md +6 -6
- package/automatization/workflows/autobuild.js +4 -1
- package/automatization/workflows/integrations/promote.js +4 -1
- package/automatization/workflows/integrations/sync.js +4 -1
- package/automatization/workflows/team.js +4 -1
- package/engine/automation/index.js +187 -36
- package/engine/cli/ops.js +32 -25
- package/engine/core/manifest.js +42 -11
- package/engine/core/ownership.js +30 -11
- package/package.json +1 -1
- package/template/AGENTS.md +3 -2
- package/template/automatization/README.md +23 -8
- package/template/tools/ops.js +6 -0
- package/template/automatization/runners/README.md +0 -13
- package/template/automatization/workflows/.gitkeep +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,73 @@ 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.1] - 2026-08-15
|
|
12
|
+
|
|
13
|
+
### Agregado
|
|
14
|
+
|
|
15
|
+
- **Codex recibe su `AGENTS.md`.** Era el único runner sin archivo de instrucciones: se llevaba los
|
|
16
|
+
guards y nada más, así que podía ser detenido pero no sabía que existía un protocolo, un catálogo de
|
|
17
|
+
cargos ni equipos. No existe un `CODEX.md`: `AGENTS.md` es el nombre que Codex lee, compartido entre
|
|
18
|
+
herramientas, y por eso en modo embedded no se instala —el de la empresa ya está ahí y manda—.
|
|
19
|
+
|
|
20
|
+
### Corregido
|
|
21
|
+
|
|
22
|
+
- **Las rutas de los adaptadores se resuelven contra la carpeta donde se abre la herramienta.** Al
|
|
23
|
+
mover la instalación a la raíz de la compañía quedaron apuntando al lugar equivocado: `@AGENTS.md`
|
|
24
|
+
y `@planning/PROTOCOL.md` no resolvían, y los workflows buscaban `planning/` donde no estaba. Ahora
|
|
25
|
+
cada fuente marca el lugar con `{{OPS_DIR}}` y `install` lo completa; `doctor` compara contra lo
|
|
26
|
+
mismo que `install` escribe.
|
|
27
|
+
- `tools/ops.js` exporta la raíz ops, que ya calculaba y no usaba. Invocado desde la carpeta de la
|
|
28
|
+
compañía —`node <empresa>-ops/tools/ops.js team list`—, `agents list` y `team list` resolvían contra
|
|
29
|
+
el cwd y devolvían **vacío en vez de fallar**. `team` además no aceptaba una raíz de ningún modo.
|
|
30
|
+
|
|
31
|
+
## [0.5.0] - 2026-08-15
|
|
32
|
+
|
|
33
|
+
### Cambiado
|
|
34
|
+
|
|
35
|
+
- **Los adaptadores de runner y los workflows tampoco se copian al proyecto.** Cierran el mismo
|
|
36
|
+
criterio que ya rige para cargos y equipos: los lee el motor, no la empresa. `automation install`,
|
|
37
|
+
`check` y `doctor` los resuelven desde la dependencia npm, o desde `.ops/` cuando el repo no usa
|
|
38
|
+
npm. Nadie los editaba —la lista de runners es cerrada, así que una empresa ni siquiera podía
|
|
39
|
+
agregar el suyo— y `automatization/workflows/` estaba además duplicado dentro de cada instancia,
|
|
40
|
+
idéntico a lo que `automation install` deja en `.claude/workflows/`.
|
|
41
|
+
- `upgrade` retira `automatization/runners/` y `automatization/workflows/` de las instancias que los
|
|
42
|
+
arrastran de una versión anterior. Un guard propio en `automatization/hooks/` no se toca.
|
|
43
|
+
- `automatization/hooks/` se queda en el proyecto, y esto no es una excepción arbitraria: la
|
|
44
|
+
configuración de cada runner nombra cada guard por ruta literal y no sabe resolver en cascada. Ahí
|
|
45
|
+
es también donde una empresa agrega el suyo.
|
|
46
|
+
|
|
47
|
+
- **En modo `sidecar`, `automation install` instala en la carpeta de la compañía, no dentro del repo
|
|
48
|
+
ops.** El repo ops coordina varios repos de producto y es hermano de ellos, así que instalar el
|
|
49
|
+
runner adentro lo dejaba sin ver una sola línea de código: el dev que abría su herramienta donde
|
|
50
|
+
está el código no tenía guards, ni cargos, ni workflows. Las rutas de guards y los punteros de cada
|
|
51
|
+
cargo se reescriben con el prefijo del repo ops al instalar.
|
|
52
|
+
|
|
53
|
+
### Corregido
|
|
54
|
+
|
|
55
|
+
- **Una mejora del toolkit en el wiring ahora llega a un runner ya instalado.** `install` conservaba
|
|
56
|
+
cualquier archivo existente, así que un workflow o un `CLAUDE.md` mejorado río arriba no llegaba
|
|
57
|
+
nunca. Peor: si el archivo difería, `install` fallaba —`existe y difiere de la fuente canónica`— y
|
|
58
|
+
`doctor` lo reportaba como error, dejando a la empresa sin salida salvo borrar a mano. Ahora el
|
|
59
|
+
registro de entrega distingue las dos cosas que antes se veían iguales: lo que la empresa editó se
|
|
60
|
+
conserva o detiene la instalación, y lo que sólo cambió río arriba se actualiza.
|
|
61
|
+
- `automation check` compara los guards de la instancia contra los del paquete. Existir y ser
|
|
62
|
+
ejecutable no alcanzaba: un guard viejo no falla, **deja de proteger en silencio**, y `check`,
|
|
63
|
+
`doctor` y `upgrade` daban verde igual porque sólo miraban el número de versión. Ahora bloquea la
|
|
64
|
+
instalación del runner y dice qué correr; distingue además el guard que la empresa editó del que
|
|
65
|
+
simplemente quedó atrás.
|
|
66
|
+
- `upgrade` recuerda reinstalar los runners: el wiring vive fuera de la instancia y lo escribe otro
|
|
67
|
+
comando, así que sin el aviso una mejora se quedaba en el paquete.
|
|
68
|
+
- Los guards resuelven la raíz ops por su cuenta. `findOpsRoot` sólo sube por el árbol, y en sidecar
|
|
69
|
+
la raíz ops es un *hermano* de los repos de producto: desde ahí no la encontraba, devolvía vacío y
|
|
70
|
+
el guard permitía todo **en silencio**. `run-hook.sh` ya sabía dónde vive; ahora lo exporta.
|
|
71
|
+
- `automatization/README.md` y `automatization/AGENTS.md` se actualizan con el toolkit. Los escribe
|
|
72
|
+
Cauce y envejecían en cada instancia: después de retirar `runners/` y `workflows/`, el README de la
|
|
73
|
+
empresa seguía explicando cómo usar dos carpetas que ya no existían.
|
|
74
|
+
- El registro de entrega olvida lo que ya no está en disco. Sólo crecía: al retirar una ruta dejaba
|
|
75
|
+
su digest para siempre, y el día que un nombre se reutilizara la entrega nueva se habría leído como
|
|
76
|
+
una edición local y detenido la actualización.
|
|
77
|
+
|
|
11
78
|
## [0.4.1] - 2026-08-15
|
|
12
79
|
|
|
13
80
|
### Corregido
|
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=""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cauce
|
|
2
2
|
|
|
3
|
-
Lee y cumple `AGENTS.md` y `planning/PROTOCOL.md` antes de ejecutar trabajo. `planning/WIP.md` es el mutex de
|
|
4
|
-
ejecución y `planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
|
|
3
|
+
Lee y cumple `AGENTS.md` y `{{OPS_DIR}}planning/PROTOCOL.md` antes de ejecutar trabajo. `{{OPS_DIR}}planning/WIP.md` es el mutex de
|
|
4
|
+
ejecución y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
|
|
5
5
|
inventes aprobaciones o credenciales y no hagas push ni deploy. Cierra cada tarea con verificación real y
|
|
6
6
|
evidencia en DONE.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Cauce para Claude Code
|
|
2
2
|
|
|
3
|
-
@AGENTS.md
|
|
4
|
-
@planning/PROTOCOL.md
|
|
3
|
+
@{{OPS_DIR}}AGENTS.md
|
|
4
|
+
@{{OPS_DIR}}planning/PROTOCOL.md
|
|
5
5
|
|
|
6
6
|
Los hooks de `.claude/settings.json` son obligatorios. Usa `/team` para evaluar si una intención es viable
|
|
7
7
|
y proponer una épica, y `/autobuild` para ejecutar trabajo ya promovido; `/integration-sync` e
|
|
8
8
|
`/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
|
|
9
9
|
|
|
10
|
-
Antes de iniciar, respeta `planning/AWAITING_REVIEW.md` y el mutex de `planning/WIP.md`. Si el protocolo y
|
|
11
|
-
un workflow difieren, manda el protocolo y la diferencia se registra en `planning/INBOX.md`.
|
|
10
|
+
Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/WIP.md`. Si el protocolo y
|
|
11
|
+
un workflow difieren, manda el protocolo y la diferencia se registra en `{{OPS_DIR}}planning/INBOX.md`.
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
{
|
|
5
5
|
"matcher": "Bash",
|
|
6
6
|
"hooks": [
|
|
7
|
-
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-shell.sh" }
|
|
7
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-shell.sh" }
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"matcher": "Edit|Write",
|
|
12
12
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-files.sh" }
|
|
13
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh" }
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
17
|
"Stop": [
|
|
18
18
|
{
|
|
19
19
|
"hooks": [
|
|
20
|
-
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-planning-drift.sh" }
|
|
20
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-planning-drift.sh" }
|
|
21
21
|
]
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Cauce para Codex
|
|
2
|
+
|
|
3
|
+
`{{OPS_DIR}}AGENTS.md` tiene las reglas del sistema y `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de
|
|
4
|
+
verdad del proceso. Leelos antes de trabajar: acá sólo está lo específico de este runner.
|
|
5
|
+
|
|
6
|
+
> Codex lee el `AGENTS.md` de la raíz, que es un nombre compartido entre herramientas. Cuando el repo
|
|
7
|
+
> ops **es** la raíz, este archivo no se instala: el `AGENTS.md` de la empresa ya está ahí y manda.
|
|
8
|
+
|
|
9
|
+
Los hooks de `.codex/hooks/hooks.json` son obligatorios y bloquean por su cuenta. Codex no ejecuta los
|
|
10
|
+
workflows JS de Claude —son referencia, no un runtime compatible—, así que el recorrido se hace fase por
|
|
11
|
+
fase siguiendo el protocolo.
|
|
12
|
+
|
|
13
|
+
`{{OPS_DIR}}planning/WIP.md` es el mutex: una sola tarea activa. `{{OPS_DIR}}planning/AWAITING_REVIEW.md`
|
|
14
|
+
bloquea una corrida nueva hasta que un humano revise. Nada se promueve desde `INBOX.md` sin aprobación.
|
|
15
|
+
|
|
16
|
+
Antes de cerrar, corré `node {{OPS_DIR}}tools/ops.js check {{OPS_DIR}}planning`.
|
|
17
|
+
|
|
18
|
+
## Los cargos
|
|
19
|
+
|
|
20
|
+
El catálogo está en `{{OPS_DIR}}node_modules/@ingeniomaps/cauce/agents/` —o en
|
|
21
|
+
`{{OPS_DIR}}.ops/agents/` si este repo no usa npm— y los propios de la empresa en
|
|
22
|
+
`{{OPS_DIR}}agents/`, que mandan sobre los del sistema con el mismo nombre.
|
|
23
|
+
|
|
24
|
+
Cada cargo tiene un `SKILL.md` con su contrato: cuándo actúa, qué decide, qué no le corresponde y cuál es
|
|
25
|
+
su entrega mínima. Para ver la lista con una línea por cargo:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node {{OPS_DIR}}tools/ops.js agents list
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Leé el `SKILL.md` del cargo que corresponda antes de actuar en su terreno, y respetá sus límites. Lo que
|
|
32
|
+
ese cargo debe saber de esta empresa está en `{{OPS_DIR}}organization/roles/<slug>.md`.
|
|
33
|
+
|
|
34
|
+
## Los equipos
|
|
35
|
+
|
|
36
|
+
Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
|
|
37
|
+
exista una épica. Están en `{{OPS_DIR}}node_modules/@ingeniomaps/cauce/teams/system/` y los propios en
|
|
38
|
+
`{{OPS_DIR}}teams/`.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node {{OPS_DIR}}tools/ops.js team list
|
|
42
|
+
node {{OPS_DIR}}tools/ops.js team show <slug>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Ningún equipo promueve trabajo al BACKLOG: escribe la épica o el informe y para.
|
|
46
|
+
|
|
47
|
+
Nunca omitas aprobaciones, inventes credenciales, escribas en un sistema remoto, hagas push o deploy.
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
{
|
|
5
5
|
"matcher": "shell|unified_exec",
|
|
6
6
|
"hooks": [
|
|
7
|
-
{ "type": "command", "command": "automatization/hooks/guard-shell.sh" }
|
|
7
|
+
{ "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-shell.sh" }
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"matcher": "apply_patch|files",
|
|
12
12
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "automatization/hooks/guard-files.sh" }
|
|
13
|
+
{ "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-files.sh" }
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
17
|
"SessionEnd": [
|
|
18
18
|
{
|
|
19
19
|
"hooks": [
|
|
20
|
-
{ "type": "command", "command": "automatization/hooks/guard-planning-drift.sh" }
|
|
20
|
+
{ "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-planning-drift.sh" }
|
|
21
21
|
]
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Cauce para Gemini CLI
|
|
2
2
|
|
|
3
|
-
@AGENTS.md
|
|
4
|
-
@planning/PROTOCOL.md
|
|
3
|
+
@{{OPS_DIR}}AGENTS.md
|
|
4
|
+
@{{OPS_DIR}}planning/PROTOCOL.md
|
|
5
5
|
|
|
6
|
-
`planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/ops:autobuild` fase por fase; los workflows JS de
|
|
7
|
-
Claude son referencia, no un runtime compatible. `planning/WIP.md` es el mutex y
|
|
8
|
-
`planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
|
|
6
|
+
`{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/ops:autobuild` fase por fase; los workflows JS de
|
|
7
|
+
Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/WIP.md` es el mutex y
|
|
8
|
+
`{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
|
|
9
9
|
|
|
10
10
|
Gemini no tiene guards nativos configurados por este toolkit. Antes de cada commit ejecuta los wrappers de
|
|
11
|
-
`automatization/hooks/` como prechecks y al cerrar ejecuta `node tools/ops.js check planning`. Nunca omitas
|
|
11
|
+
`{{OPS_DIR}}automatization/hooks/` como prechecks y al cerrar ejecuta `node {{OPS_DIR}}tools/ops.js check planning`. Nunca omitas
|
|
12
12
|
aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones trabajo desde INBOX.
|
|
@@ -10,7 +10,10 @@ export const meta = {
|
|
|
10
10
|
].map((title) => ({ title, detail: `Fase ${title} del protocolo agnóstico` })),
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
|
|
14
|
+
// de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
|
|
15
|
+
const ROOT = (process.env.OPS_ROOT
|
|
16
|
+
|| `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
|
|
14
17
|
const CONFIG = `${ROOT}/ops.config.json`
|
|
15
18
|
const P = `${ROOT}/planning`
|
|
16
19
|
const BACKLOG = `${P}/BACKLOG.md`
|
|
@@ -10,7 +10,10 @@ export const meta = {
|
|
|
10
10
|
],
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
|
|
14
|
+
// de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
|
|
15
|
+
const ROOT = (process.env.OPS_ROOT
|
|
16
|
+
|| `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
|
|
14
17
|
const PROVIDER = process.env.OPS_INTEGRATION_PROVIDER || ''
|
|
15
18
|
const KEY = process.env.OPS_INTEGRATION_KEY || ''
|
|
16
19
|
const RESULT = {
|
|
@@ -10,7 +10,10 @@ export const meta = {
|
|
|
10
10
|
],
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
|
|
14
|
+
// de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
|
|
15
|
+
const ROOT = (process.env.OPS_ROOT
|
|
16
|
+
|| `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
|
|
14
17
|
const REQUESTED = process.env.OPS_INTEGRATION_PROVIDER || ''
|
|
15
18
|
const RESULT = {
|
|
16
19
|
type: 'object', additionalProperties: false, required: ['passed', 'provider', 'details'],
|
|
@@ -15,7 +15,10 @@ export const meta = {
|
|
|
15
15
|
],
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
|
|
19
|
+
// de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
|
|
20
|
+
const ROOT = (process.env.OPS_ROOT
|
|
21
|
+
|| `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
|
|
19
22
|
const P = `${ROOT}/planning`
|
|
20
23
|
const ROADMAP = `${P}/roadmap`
|
|
21
24
|
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
@@ -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,37 @@ 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
|
+
// Todo lo que un adaptador copia —configuración, instrucciones, workflows— nombra rutas relativas a
|
|
126
|
+
// la carpeta donde se abre la herramienta. Cuando la raíz ops no es esa carpeta, cada una necesita el
|
|
127
|
+
// prefijo. El marcador es explícito en la fuente en vez de adivinarse con reemplazos de texto:
|
|
128
|
+
// `{{OPS_DIR}}` significa "acá va la raíz ops, o nada si coinciden".
|
|
129
|
+
//
|
|
130
|
+
// Un solo render, y `install` escribe exactamente lo que `doctor` compara.
|
|
131
|
+
const OPS_DIR = '{{OPS_DIR}}'
|
|
132
|
+
|
|
133
|
+
function render(file, prefix) {
|
|
134
|
+
return fs.readFileSync(file, 'utf8').split(OPS_DIR).join(prefix)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function runnerConfig(paths, root) {
|
|
138
|
+
return JSON.parse(render(paths.configSource, opsPrefix(root)))
|
|
139
|
+
}
|
|
140
|
+
|
|
87
141
|
// Cargos del catálogo, con el frontmatter que el runner indexa para elegir a quién invocar.
|
|
88
142
|
function roleCatalog(root) {
|
|
89
143
|
return catalog.list(root)
|
|
90
144
|
.map((role) => {
|
|
91
145
|
const field = P.frontmatter(fs.readFileSync(path.join(role.dir, 'SKILL.md'), 'utf8'))
|
|
92
|
-
|
|
146
|
+
const reference = path.relative(installRoot(root), role.dir).split(path.sep).join('/')
|
|
147
|
+
return { ...role, reference, description: field('description') }
|
|
93
148
|
})
|
|
94
149
|
.filter((role) => role.description)
|
|
95
150
|
}
|
|
@@ -115,11 +170,12 @@ Respetá los límites de ese contrato y las reglas de \`AGENTS.md\`. Generado po
|
|
|
115
170
|
|
|
116
171
|
function installRoleSkills(root, runner, output) {
|
|
117
172
|
if (!runner.capabilities.nativeSkills || !runner.roleSkills) return 0
|
|
118
|
-
const
|
|
173
|
+
const install = installRoot(root)
|
|
174
|
+
const base = F.assertWithin(install, path.resolve(install, runner.roleSkills), `${runner.name}: roleSkills`)
|
|
119
175
|
const roles = roleCatalog(root)
|
|
120
176
|
for (const role of roles) {
|
|
121
177
|
const file = path.join(base, role.slug, 'SKILL.md')
|
|
122
|
-
F.assertNoSymlinkPath(
|
|
178
|
+
F.assertNoSymlinkPath(install, file)
|
|
123
179
|
F.atomicWrite(file, roleSkill(role))
|
|
124
180
|
}
|
|
125
181
|
if (roles.length) output.log(`✓ ${runner.name}: ${roles.length} cargo(s) disponibles en ${runner.roleSkills}`)
|
|
@@ -141,6 +197,30 @@ function hasHooks(config) {
|
|
|
141
197
|
})
|
|
142
198
|
}
|
|
143
199
|
|
|
200
|
+
// Guards de la instancia que ya no coinciden con los del paquete. Existir y ser ejecutable no
|
|
201
|
+
// alcanza: un guard viejo no falla, deja de proteger sin decir nada. La instancia declaraba una
|
|
202
|
+
// versión y nadie comprobaba que su runtime fuera realmente esa.
|
|
203
|
+
function staleHooks(root) {
|
|
204
|
+
const packaged = packagedAutomation(root)
|
|
205
|
+
if (!packaged) return []
|
|
206
|
+
const shipped = path.join(packaged, 'hooks')
|
|
207
|
+
const mine = path.join(root, 'automatization', 'hooks')
|
|
208
|
+
const recorded = M.read(root)
|
|
209
|
+
const stale = []
|
|
210
|
+
let names = []
|
|
211
|
+
try { names = fs.readdirSync(shipped) } catch { return [] }
|
|
212
|
+
for (const file of names) {
|
|
213
|
+
const local = path.join(mine, file)
|
|
214
|
+
// Los que faltan ya los reporta el chequeo de arriba; acá sólo interesa el que quedó atrás.
|
|
215
|
+
if (!fs.existsSync(local)) continue
|
|
216
|
+
const current = M.digest(local)
|
|
217
|
+
if (current === M.digest(path.join(shipped, file))) continue
|
|
218
|
+
const delivered = recorded[`automatization/hooks/${file}`]
|
|
219
|
+
stale.push({ file, edited: Boolean(delivered) && delivered !== current })
|
|
220
|
+
}
|
|
221
|
+
return stale
|
|
222
|
+
}
|
|
223
|
+
|
|
144
224
|
function check(root) {
|
|
145
225
|
const errors = []
|
|
146
226
|
const hookDir = path.join(root, 'automatization', 'hooks')
|
|
@@ -176,11 +256,19 @@ function check(root) {
|
|
|
176
256
|
}
|
|
177
257
|
const workflows = ['autobuild.js', 'team.js', path.join('integrations', 'sync.js')]
|
|
178
258
|
workflows.push(path.join('integrations', 'promote.js'))
|
|
259
|
+
const packaged = packagedAutomation(root)
|
|
179
260
|
for (const name of workflows) {
|
|
180
|
-
if (!fs.existsSync(path.join(
|
|
181
|
-
errors.push(`falta automatization/workflows/${name}
|
|
261
|
+
if (!packaged || !fs.existsSync(path.join(packaged, 'workflows', name))) {
|
|
262
|
+
errors.push(`falta automatization/workflows/${name}: instalá la dependencia o restaurá .ops/`)
|
|
182
263
|
}
|
|
183
264
|
}
|
|
265
|
+
for (const { file, edited } of staleHooks(root)) {
|
|
266
|
+
errors.push(edited
|
|
267
|
+
? `automatization/hooks/${file}: lo editaste y es del toolkit; agregá un guard propio al lado `
|
|
268
|
+
+ 'o descartá tu cambio con `cauce upgrade --force`'
|
|
269
|
+
: `automatization/hooks/${file}: quedó atrás del paquete y ya no protege lo que dice; `
|
|
270
|
+
+ 'corré `cauce upgrade` antes de instalar el runner')
|
|
271
|
+
}
|
|
184
272
|
for (const name of RUNNER_NAMES) validateRunner(root, name, errors)
|
|
185
273
|
return errors
|
|
186
274
|
}
|
|
@@ -194,7 +282,7 @@ function validateRunner(root, name, errors) {
|
|
|
194
282
|
return
|
|
195
283
|
}
|
|
196
284
|
const paths = runnerPaths(root, name, runner)
|
|
197
|
-
const config =
|
|
285
|
+
const config = runnerConfig(paths, root)
|
|
198
286
|
if (runner.capabilities.nativeHooks && !hasHooks(config)) {
|
|
199
287
|
errors.push(`${name}: declara hooks nativos pero no los configura`)
|
|
200
288
|
}
|
|
@@ -272,7 +360,7 @@ function doctor(root, name, output = console) {
|
|
|
272
360
|
const errors = []
|
|
273
361
|
const warnings = []
|
|
274
362
|
try {
|
|
275
|
-
const expected =
|
|
363
|
+
const expected = runnerConfig(paths, root)
|
|
276
364
|
const actual = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
277
365
|
if (!includesConfig(actual, expected)) {
|
|
278
366
|
errors.push(`${runner.config.target}: configuración instalada incompleta o divergente`)
|
|
@@ -286,18 +374,31 @@ function doctor(root, name, output = console) {
|
|
|
286
374
|
errors.push(`${runner.config.target}: ${error.message}`)
|
|
287
375
|
}
|
|
288
376
|
for (const item of runner.instructions || []) {
|
|
289
|
-
const resolved = resolveItem(paths, root, name, item)
|
|
377
|
+
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
378
|
+
// `AGENTS.md` es un nombre compartido entre herramientas y la instancia ya tiene el suyo: en
|
|
379
|
+
// modo embedded el archivo del runner y el de la empresa son el mismo, y pedirle que se
|
|
380
|
+
// referencie a sí mismo no significa nada.
|
|
381
|
+
const propio = path.basename(resolved.target) === 'AGENTS.md'
|
|
290
382
|
if (!fs.existsSync(resolved.target)) errors.push(`falta ${item.target}`)
|
|
291
|
-
else if (!fs.readFileSync(resolved.target, 'utf8').includes('AGENTS.md')) {
|
|
383
|
+
else if (!propio && !fs.readFileSync(resolved.target, 'utf8').includes('AGENTS.md')) {
|
|
292
384
|
warnings.push(`${item.target}: no referencia AGENTS.md; verifica las reglas globales`)
|
|
293
385
|
}
|
|
386
|
+
if (deliveryState(M.readRunners(root), name, resolved, opsPrefix(root)) === 'desactualizado') {
|
|
387
|
+
warnings.push(`${item.target}: Cauce trae una versión más nueva y vos no lo tocaste; reinstalá`)
|
|
388
|
+
}
|
|
294
389
|
}
|
|
390
|
+
// Divergir no es un error: puede ser una mejora río arriba esperando reinstalación. Reportarlo
|
|
391
|
+
// como error dejaba a la empresa sin salida, porque `install` tampoco lo actualizaba.
|
|
392
|
+
const recorded = M.readRunners(root)
|
|
295
393
|
for (const item of runner.artifacts || []) {
|
|
296
|
-
const resolved = resolveItem(paths, root, name, item)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
394
|
+
const resolved = { item, ...resolveItem(paths, root, name, item) }
|
|
395
|
+
const situacion = deliveryState(recorded, name, resolved, opsPrefix(root))
|
|
396
|
+
if (situacion === 'nuevo') errors.push(`falta ${item.target}`)
|
|
397
|
+
else if (situacion === 'desactualizado') {
|
|
398
|
+
warnings.push(`${item.target}: hay una versión más nueva en Cauce; reinstalá el adaptador`)
|
|
399
|
+
} else if (situacion === 'ajeno') {
|
|
400
|
+
warnings.push(`${item.target}: lo editaste y es del toolkit; `
|
|
401
|
+
+ 'agregá lo tuyo al lado o reinstalá con --force para volver a la versión de Cauce')
|
|
301
402
|
}
|
|
302
403
|
}
|
|
303
404
|
// Un cargo que quedó fuera, o cuya descripción cambió en el catálogo, deja al runner eligiendo
|
|
@@ -306,7 +407,7 @@ function doctor(root, name, output = console) {
|
|
|
306
407
|
const missing = []
|
|
307
408
|
const stale = []
|
|
308
409
|
for (const role of roleCatalog(root)) {
|
|
309
|
-
const file = path.join(
|
|
410
|
+
const file = path.join(paths.install, runner.roleSkills, role.slug, 'SKILL.md')
|
|
310
411
|
if (!fs.existsSync(file)) missing.push(role.slug)
|
|
311
412
|
else if (fs.readFileSync(file, 'utf8') !== roleSkill(role)) stale.push(role.slug)
|
|
312
413
|
}
|
|
@@ -327,12 +428,35 @@ function doctor(root, name, output = console) {
|
|
|
327
428
|
return { runner, errors, warnings }
|
|
328
429
|
}
|
|
329
430
|
|
|
330
|
-
|
|
431
|
+
// Clave de entrega de un archivo del adaptador. Va en su propia sección del manifiesto porque puede
|
|
432
|
+
// caer fuera de la instancia: en sidecar el wiring vive en la carpeta de la compañía.
|
|
433
|
+
function deliveryKey(name, target) {
|
|
434
|
+
return `${name}/${target.split(path.sep).join('/')}`
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// En qué estado quedó un archivo que este adaptador entregó alguna vez.
|
|
438
|
+
//
|
|
439
|
+
// nuevo no existe todavía
|
|
440
|
+
// al día idéntico a lo que trae Cauce
|
|
441
|
+
// desactualizado la empresa no lo tocó, pero río arriba cambió
|
|
442
|
+
// ajeno difiere de lo entregado: alguien lo editó acá
|
|
443
|
+
//
|
|
444
|
+
// Sin el registro de entrega, `desactualizado` y `ajeno` se ven igual. `install` resolvía esa duda
|
|
445
|
+
// conservando siempre, así que ninguna mejora del toolkit llegaba nunca a un runner ya instalado.
|
|
446
|
+
function deliveryState(recorded, name, resolved, prefix = '') {
|
|
447
|
+
if (!fs.existsSync(resolved.target)) return 'nuevo'
|
|
448
|
+
const current = M.digest(resolved.target)
|
|
449
|
+
if (current === M.digestText(render(resolved.source, prefix))) return 'al día'
|
|
450
|
+
const delivered = recorded[deliveryKey(name, resolved.item.target)]
|
|
451
|
+
return delivered && delivered === current ? 'desactualizado' : 'ajeno'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function install(root, name, output = console, options = {}) {
|
|
331
455
|
const runner = runnerManifest(root, name)
|
|
332
456
|
const errors = check(root)
|
|
333
457
|
if (errors.length) throw new Error(`La automatización no es instalable:\n- ${errors.join('\n- ')}`)
|
|
334
458
|
const paths = runnerPaths(root, name, runner)
|
|
335
|
-
const incoming =
|
|
459
|
+
const incoming = runnerConfig(paths, root)
|
|
336
460
|
let current = {}
|
|
337
461
|
if (fs.existsSync(paths.configTarget)) {
|
|
338
462
|
try { current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8')) } catch (error) {
|
|
@@ -341,31 +465,58 @@ function install(root, name, output = console) {
|
|
|
341
465
|
}
|
|
342
466
|
const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
|
|
343
467
|
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
|
-
|
|
468
|
+
F.assertNoSymlinkPath(paths.install, paths.configTarget)
|
|
469
|
+
for (const resolved of resolvedItems) F.assertNoSymlinkPath(paths.install, resolved.target)
|
|
470
|
+
|
|
471
|
+
const recorded = M.readRunners(root)
|
|
472
|
+
const prefix = opsPrefix(root)
|
|
473
|
+
const state = new Map(resolvedItems.map((r) => [r, deliveryState(recorded, name, r, prefix)]))
|
|
474
|
+
// Un archivo de instrucciones es del proyecto y se conserva; uno ejecutable es del toolkit y se
|
|
475
|
+
// reemplaza. Editarlo detiene la instalación antes de pisarlo, igual que hace `upgrade`.
|
|
476
|
+
const editados = resolvedItems.filter((resolved) => {
|
|
477
|
+
return state.get(resolved) === 'ajeno' && !runner.instructions.includes(resolved.item)
|
|
478
|
+
})
|
|
479
|
+
if (editados.length && !options.force) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`${editados.length} archivo(s) que mantiene Cauce fueron editados y se perderían:\n`
|
|
482
|
+
+ `${editados.map((resolved) => `- ${resolved.item.target}`).join('\n')}\n\n`
|
|
483
|
+
+ 'Son del toolkit: en vez de editarlos, agregá lo tuyo al lado y registralo en la\n'
|
|
484
|
+
+ 'configuración de tu runner. Si el cambio ya no te sirve, repetí con --force.',
|
|
485
|
+
)
|
|
352
486
|
}
|
|
353
487
|
const merged = pruneSupersededHooks(mergeConfig(current, incoming))
|
|
354
488
|
F.atomicWriteJson(paths.configTarget, merged.config)
|
|
355
489
|
for (const { wrapper, files } of merged.replaced) {
|
|
356
490
|
output.log(`− ${name}: reemplazado ${[...new Set(files)].join(', ')} por ${wrapper}`)
|
|
357
491
|
}
|
|
492
|
+
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
493
|
+
// el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
|
|
494
|
+
if (paths.install !== root) {
|
|
495
|
+
output.log(` ${name}: el runner se abre en ${paths.install} — ahí queda su configuración`)
|
|
496
|
+
}
|
|
358
497
|
output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
|
|
498
|
+
const entregado = { ...recorded }
|
|
359
499
|
for (const resolved of resolvedItems) {
|
|
360
|
-
|
|
361
|
-
|
|
500
|
+
const situacion = state.get(resolved)
|
|
501
|
+
const propio = runner.instructions.includes(resolved.item)
|
|
502
|
+
if (situacion === 'ajeno' && propio) {
|
|
503
|
+
output.log(`= ${name}: conservado ${resolved.item.target} (tiene cambios tuyos)`)
|
|
504
|
+
} else if (situacion === 'al día') {
|
|
505
|
+
output.log(`= ${name}: ${resolved.item.target} ya está al día`)
|
|
362
506
|
} else {
|
|
363
507
|
fs.mkdirSync(path.dirname(resolved.target), { recursive: true })
|
|
364
|
-
|
|
365
|
-
|
|
508
|
+
F.atomicWrite(resolved.target, render(resolved.source, opsPrefix(root)))
|
|
509
|
+
const verbo = situacion === 'nuevo' ? 'instalado' : 'actualizado'
|
|
510
|
+
output.log(`✓ ${name}: ${verbo} ${resolved.item.target}`)
|
|
511
|
+
}
|
|
512
|
+
// Sólo se anota lo que Cauce puso: un archivo conservado con cambios de la empresa no es una
|
|
513
|
+
// entrega, y registrarlo lo volvería indistinguible de uno intacto en la próxima instalación.
|
|
514
|
+
if (!(situacion === 'ajeno' && propio)) {
|
|
515
|
+
entregado[deliveryKey(name, resolved.item.target)] = M.digest(resolved.target)
|
|
366
516
|
}
|
|
367
517
|
}
|
|
368
518
|
installRoleSkills(root, runner, output)
|
|
519
|
+
M.write(root, undefined, entregado)
|
|
369
520
|
return runner
|
|
370
521
|
}
|
|
371
522
|
|
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,13 +582,27 @@ 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—
|
|
596
596
|
// reimplemente el recorrido del catálogo.
|
|
597
|
+
// La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
|
|
598
|
+
// vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
|
|
599
|
+
function opsRoot(dir) {
|
|
600
|
+
return path.resolve(dir || process.env.OPS_ROOT || '.')
|
|
601
|
+
}
|
|
602
|
+
|
|
597
603
|
function agents(action, dir) {
|
|
598
604
|
if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
|
|
599
|
-
const root =
|
|
605
|
+
const root = opsRoot(dir)
|
|
600
606
|
const roles = AG.list(root)
|
|
601
607
|
if (process.argv.includes('--json')) {
|
|
602
608
|
// `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
|
|
@@ -685,7 +691,7 @@ async function integration(action, rootArg, provider, key) {
|
|
|
685
691
|
}
|
|
686
692
|
|
|
687
693
|
function automation(action, rootArg, runnerName) {
|
|
688
|
-
const root =
|
|
694
|
+
const root = opsRoot(rootArg)
|
|
689
695
|
if (action === 'list-hooks') return A.listHooks()
|
|
690
696
|
if (action === 'list') {
|
|
691
697
|
for (const name of A.RUNNER_NAMES) {
|
|
@@ -717,7 +723,8 @@ function automation(action, rootArg, runnerName) {
|
|
|
717
723
|
}
|
|
718
724
|
if (action === 'install') {
|
|
719
725
|
let runner
|
|
720
|
-
|
|
726
|
+
const force = process.argv.includes('--force')
|
|
727
|
+
try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
|
|
721
728
|
if (runnerName === 'codex') {
|
|
722
729
|
console.log(' Codex pedirá revisar y confiar en hooks nuevos al iniciar sesión.')
|
|
723
730
|
}
|
|
@@ -755,12 +762,12 @@ function evaluate(agent) {
|
|
|
755
762
|
|
|
756
763
|
function team(action, slug) {
|
|
757
764
|
if (action === 'list') {
|
|
758
|
-
for (const name of T.list(
|
|
765
|
+
for (const name of T.list(opsRoot())) console.log(name)
|
|
759
766
|
return
|
|
760
767
|
}
|
|
761
768
|
if (!['check', 'show'].includes(action)) fail(`Acción de team desconocida: ${action || '(vacía)'}`, 2)
|
|
762
769
|
try {
|
|
763
|
-
const result = T.validate(
|
|
770
|
+
const result = T.validate(opsRoot(), slug)
|
|
764
771
|
for (const error of result.errors) console.error(`✗ ${error}`)
|
|
765
772
|
if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
|
|
766
773
|
if (action === 'show') {
|
package/engine/core/manifest.js
CHANGED
|
@@ -13,24 +13,46 @@ const path = require('node:path')
|
|
|
13
13
|
|
|
14
14
|
const FILE = path.join('.cauce', 'manifest.json')
|
|
15
15
|
|
|
16
|
+
function digestText(content) {
|
|
17
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16)
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
function digest(file) {
|
|
17
|
-
try {
|
|
18
|
-
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16)
|
|
19
|
-
} catch { return '' }
|
|
21
|
+
try { return digestText(fs.readFileSync(file)) } catch { return '' }
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
// Dos secciones porque son dos entregas distintas: `files` es lo que se materializó dentro de la
|
|
25
|
+
// instancia y `runners` lo que un adaptador dejó fuera de ella —en modo sidecar el wiring vive en
|
|
26
|
+
// la carpeta de la compañía—. El registro se queda igual acá, que es el repo que la empresa versiona.
|
|
27
|
+
function readAll(root) {
|
|
23
28
|
try {
|
|
24
|
-
const data = JSON.parse(fs.readFileSync(path.join(root, FILE), 'utf8'))
|
|
25
|
-
return
|
|
26
|
-
|
|
29
|
+
const data = JSON.parse(fs.readFileSync(path.join(root, FILE), 'utf8')) || {}
|
|
30
|
+
return {
|
|
31
|
+
files: typeof data.files === 'object' && data.files ? data.files : {},
|
|
32
|
+
runners: typeof data.runners === 'object' && data.runners ? data.runners : {},
|
|
33
|
+
}
|
|
34
|
+
} catch { return { files: {}, runners: {} } }
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
function
|
|
37
|
+
function read(root) { return readAll(root).files }
|
|
38
|
+
|
|
39
|
+
function readRunners(root) { return readAll(root).runners }
|
|
40
|
+
|
|
41
|
+
// Cada sección que no se pasa se conserva: `upgrade` no sabe del wiring y `install` no sabe de la
|
|
42
|
+
// instancia, y ninguno de los dos debería borrar lo que el otro anotó.
|
|
43
|
+
function write(root, files, runners) {
|
|
44
|
+
const current = readAll(root)
|
|
30
45
|
const target = path.join(root, FILE)
|
|
31
46
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
32
|
-
const ordered =
|
|
33
|
-
|
|
47
|
+
const ordered = (map) => Object.fromEntries(
|
|
48
|
+
Object.entries(map).sort(([left], [right]) => left.localeCompare(right)),
|
|
49
|
+
)
|
|
50
|
+
const data = {
|
|
51
|
+
version: 1,
|
|
52
|
+
files: ordered(files || current.files),
|
|
53
|
+
runners: ordered(runners || current.runners),
|
|
54
|
+
}
|
|
55
|
+
fs.writeFileSync(target, `${JSON.stringify(data, null, 2)}\n`)
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
// Registra lo entregado en una ruta, relativo a la raíz de la instancia.
|
|
@@ -40,6 +62,15 @@ function record(root, relative, files) {
|
|
|
40
62
|
return current
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
// Olvida lo que ya no está en disco. Sin esto el registro sólo crece: una ruta retirada deja su
|
|
66
|
+
// digest para siempre, y el día que un nombre se reutilice la entrega nueva se leería como una
|
|
67
|
+
// edición local y detendría la actualización.
|
|
68
|
+
function prune(root, files) {
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.entries(files).filter(([file]) => fs.existsSync(path.join(root, file))),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
// Archivos que la empresa modificó después de recibirlos. Un archivo sin registro previo no
|
|
44
75
|
// cuenta: llegó con una versión anterior a este mecanismo, o lo agregó el proyecto.
|
|
45
76
|
function edited(root, relative, files) {
|
|
@@ -51,4 +82,4 @@ function edited(root, relative, files) {
|
|
|
51
82
|
})
|
|
52
83
|
}
|
|
53
84
|
|
|
54
|
-
module.exports = { FILE, digest, edited, read, record, write }
|
|
85
|
+
module.exports = { FILE, digest, digestText, edited, prune, read, readRunners, record, write }
|
package/engine/core/ownership.js
CHANGED
|
@@ -25,6 +25,8 @@ const SYSTEM_FILES = [
|
|
|
25
25
|
'organization/roles/README.md',
|
|
26
26
|
'teams/000-template.md',
|
|
27
27
|
'teams/README.md',
|
|
28
|
+
'automatization/README.md',
|
|
29
|
+
'automatization/AGENTS.md',
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
// Colecciones mixtas: el toolkit posee `<dir>/system/`, el proyecto todo lo demás del directorio.
|
|
@@ -41,23 +43,27 @@ const RUNTIME_PATHS = [
|
|
|
41
43
|
'.ops/engine',
|
|
42
44
|
'.ops/agents',
|
|
43
45
|
'.ops/teams',
|
|
46
|
+
'.ops/automatization/runners',
|
|
47
|
+
'.ops/automatization/workflows',
|
|
44
48
|
'automatization/hooks',
|
|
45
|
-
'automatization/runners',
|
|
46
|
-
'automatization/workflows',
|
|
47
49
|
]
|
|
48
50
|
|
|
49
|
-
// Rutas que el paquete tiene por duplicado: una versión para el proyecto en `template/` y otra
|
|
50
|
-
// interna del toolkit. Gana la del template, que es la que le habla a quien usa la instancia.
|
|
51
|
-
const TEMPLATE_OWNED = ['automatization/runners/README.md']
|
|
52
|
-
|
|
53
51
|
// Dentro del paquete, lo que una instancia recibe en su raíz vive bajo `template/`; el catálogo,
|
|
54
52
|
// los equipos y la automatización están en la raíz del paquete, y el motor en `engine/`.
|
|
55
|
-
const TEMPLATE_PREFIXES = [
|
|
53
|
+
const TEMPLATE_PREFIXES = [
|
|
54
|
+
'planning/',
|
|
55
|
+
'organization/',
|
|
56
|
+
'integrations/',
|
|
57
|
+
'teams/',
|
|
58
|
+
'automatization/',
|
|
59
|
+
]
|
|
56
60
|
|
|
57
61
|
function sourceOf(relative) {
|
|
58
|
-
|
|
59
|
-
if (relative
|
|
60
|
-
|
|
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
|
|
61
67
|
if (TEMPLATE_PREFIXES.some((prefix) => relative.startsWith(prefix))) {
|
|
62
68
|
return path.join('template', relative)
|
|
63
69
|
}
|
|
@@ -81,6 +87,17 @@ function engineAt(root, relative = '') {
|
|
|
81
87
|
.find((candidate) => fs.existsSync(candidate)) || ''
|
|
82
88
|
}
|
|
83
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
|
+
|
|
84
101
|
// Definiciones que consume el motor —cargos y equipos— y que por eso viajan con el paquete en vez
|
|
85
102
|
// de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
|
|
86
103
|
// proyecto deba crear. Una sola implementación para las dos, o divergen.
|
|
@@ -135,6 +152,8 @@ const RETIRED = [
|
|
|
135
152
|
'agents/roles/system',
|
|
136
153
|
'teams/system',
|
|
137
154
|
'.github/workflows/agent-learning.yml',
|
|
155
|
+
'automatization/runners',
|
|
156
|
+
'automatization/workflows',
|
|
138
157
|
]
|
|
139
158
|
|
|
140
159
|
// Aprendizaje que quedó dentro de una ruta retirada. Es lo único ahí que no se puede reponer, así
|
|
@@ -205,7 +224,7 @@ module.exports = {
|
|
|
205
224
|
retiredWithLearning,
|
|
206
225
|
engineAt,
|
|
207
226
|
engineCandidates,
|
|
208
|
-
|
|
227
|
+
packagePath,
|
|
209
228
|
SYSTEM_COLLECTIONS,
|
|
210
229
|
SYSTEM_FILES,
|
|
211
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-*`.
|
package/template/tools/ops.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
const path = require('path')
|
|
9
9
|
|
|
10
10
|
const root = path.join(__dirname, '..')
|
|
11
|
+
|
|
12
|
+
// El shim sabe dónde vive; quien lo invoca, no. En modo sidecar se lo llama desde la carpeta de la
|
|
13
|
+
// compañía —`node <empresa>-ops/tools/ops.js …`— y sin esto cada comando resolvería su raíz contra
|
|
14
|
+
// el cwd: `agents list` y `team list` devolvían vacío en vez de fallar.
|
|
15
|
+
process.env.OPS_ROOT = process.env.OPS_ROOT || root
|
|
16
|
+
|
|
11
17
|
const candidates = [
|
|
12
18
|
() => require.resolve('@ingeniomaps/cauce/engine/cli/ops.js', { paths: [root] }),
|
|
13
19
|
() => require.resolve(path.join(root, '.ops', 'engine', 'cli', 'ops.js')),
|
|
@@ -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
|
-
|