@ingeniomaps/cauce 0.41.0 → 0.42.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 +43 -0
- package/automatization/runners/README.md +31 -14
- package/automatization/runners/antigravity/README.md +23 -9
- package/automatization/runners/antigravity/hook.js +79 -15
- package/automatization/runners/antigravity/hooks.json +3 -3
- package/automatization/runners/antigravity/manifest.json +3 -6
- package/automatization/runners/antigravity/rules/cauce.md +1 -1
- package/automatization/runners/antigravity/skills/onboard/SKILL.md +1 -69
- package/automatization/runners/claude/README.md +11 -5
- package/automatization/runners/codex/AGENTS.md +2 -30
- package/automatization/runners/codex/README.md +18 -7
- package/automatization/runners/codex/hooks.json +5 -5
- package/automatization/runners/codex/manifest.json +1 -1
- package/automatization/runners/gemini/GEMINI.md +8 -24
- package/automatization/runners/gemini/README.md +5 -3
- package/automatization/runners/gemini/commands/cauce/onboard.toml +1 -18
- package/automatization/runners/gemini/manifest.json +0 -4
- package/automatization/shared/onboard.md +69 -0
- package/engine/automation/index.js +117 -51
- package/engine/cli/ops.js +2 -1
- package/engine/hooks/run.js +14 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,49 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.42.0] - 2026-08-20
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Los guards de Codex no corrían nunca.** Tres cosas a la vez, y ninguna hacía ruido: el archivo se
|
|
22
|
+
instalaba en `.codex/hooks/hooks.json` —esa forma es la que empaqueta un plugin; un repositorio se lee
|
|
23
|
+
en `.codex/hooks.json`—, los `matcher` filtraban nombres del protocolo interno en vez del nombre de la
|
|
24
|
+
herramienta (`Bash`, `apply_patch`/`Edit`/`Write`), y el comando era una ruta relativa mientras Codex
|
|
25
|
+
ejecuta el hook con el cwd de la sesión, así que desde cualquier subdirectorio no existía. **Reinstalá
|
|
26
|
+
el adaptador**, borrá a mano el `.codex/hooks/hooks.json` que queda huérfano, y confiá los hooks con
|
|
27
|
+
`/hooks` dentro de una sesión: Codex los saltea en silencio hasta que lo hagas, y hay que repetirlo
|
|
28
|
+
cada vez que el wiring cambie.
|
|
29
|
+
- **El guard de archivos no veía nada bajo Codex.** `apply_patch` manda el parche entero como
|
|
30
|
+
`tool_input.command`, no como `patch`, y sin reconocer ese campo el guard miraba una escritura y no
|
|
31
|
+
encontraba un solo archivo: reescribió una migración existente en una corrida real, y lo mismo valía
|
|
32
|
+
para secretos y para los límites del workspace.
|
|
33
|
+
- **Los guards de Antigravity tampoco corrían.** `agy` ejecuta la copia del plugin que registró en
|
|
34
|
+
`~/.gemini/config/plugins/` y resuelve las rutas contra la carpeta del plugin, así que la del
|
|
35
|
+
workspace apuntaba a un módulo inexistente; su payload además no nombra el workspace —manda
|
|
36
|
+
`workspacePaths` vacío y un `Cwd` que apunta al scratch del CLI o al home—, de modo que la raíz ops
|
|
37
|
+
ahora se escribe como ruta absoluta al instalar. **Reinstalá y volvé a registrar** con
|
|
38
|
+
`agy plugin install .agents/plugins/cauce` cada vez que cambie el wiring.
|
|
39
|
+
- **Un puente que no arrancaba dejaba al agente sin poder cerrar la sesión.** En Antigravity, un guard
|
|
40
|
+
que bloquea y un fallo de infraestructura devolvían los dos `continue`; ahora sólo el bloqueo lo hace,
|
|
41
|
+
y la falla deja cerrar con la razón a la vista.
|
|
42
|
+
- **Los guards resolvían rutas relativas contra la carpeta equivocada en Antigravity**, que no es la del
|
|
43
|
+
proyecto: un guard que juzgaba `src/x.js` estaba juzgando otro archivo.
|
|
44
|
+
- **Instalar acumulaba las entradas de hooks de versiones anteriores.** Una ruta nuestra que cambiaba
|
|
45
|
+
dejaba viva la anterior, el runner la ejecutaba, no encontraba nada y el guard no corría. Ahora se
|
|
46
|
+
reemplazan las nuestras y se conserva lo que agregó el proyecto.
|
|
47
|
+
- **Un cargo con el mismo nombre que un recorrido lo pisaba en silencio**, porque comparten el espacio de
|
|
48
|
+
skills del runner. La instalación ahora se detiene y dice cuál: renombrá el cargo en `agents/roles/`.
|
|
49
|
+
- **Gemini invocaba `/ops:autobuild`**, un prefijo que ya no existe, y **Antigravity anunciaba sus
|
|
50
|
+
recorridos sin la barra** (`cauce:onboard` en vez de `/cauce:onboard`).
|
|
51
|
+
- **La regla de Antigravity apuntaba a un `AGENTS.md` sin prefijo**, que en modo sidecar es el del
|
|
52
|
+
repositorio de producto y no el que trae las reglas del sistema.
|
|
53
|
+
|
|
54
|
+
### Cambiado
|
|
55
|
+
|
|
56
|
+
- **El arranque de Gemini vive ahora en su comando.** `GEMINI.md` remite a `/cauce:onboard` en vez de
|
|
57
|
+
repetir el recorrido, y el comando trae la lista completa de lo que se comprueba al final —incluidos
|
|
58
|
+
los dos puntos que la copia había perdido—.
|
|
59
|
+
|
|
17
60
|
## [0.41.0] - 2026-08-19
|
|
18
61
|
|
|
19
62
|
### Corregido
|
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
# Adaptadores de runners
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cada adaptador traduce el mismo toolkit al espacio de nombres de su herramienta. Lo que declara su
|
|
4
|
+
`manifest.json`:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
| Campo | Qué resuelve |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `command` | el ejecutable con el que se comprueba que la herramienta esté disponible |
|
|
9
|
+
| `config` | de qué archivo sale el wiring de hooks y en qué ruta del proyecto se instala |
|
|
10
|
+
| `instructions` | qué archivo de contexto se copia, si la herramienta lee uno propio |
|
|
11
|
+
| `artifacts` | los recorridos y cualquier archivo que el adaptador aporte |
|
|
12
|
+
| `capabilities` | qué ejecuta de verdad —`nativeHooks`, `nativeWorkflows`, `nativeSkills`, `checkpointing`, `projectInstructions`— y qué queda como protocolo manual |
|
|
13
|
+
| `roleSkills` | dónde se instala el catálogo de cargos, cuando la herramienta tiene skills |
|
|
14
|
+
| `activation` | el paso manual que lo pone a correr, cómo verificarlo y qué se espera ver |
|
|
15
|
+
| `commands` | los nombres de los recorridos y con qué prefijo se los invoca acá |
|
|
16
|
+
|
|
17
|
+
Lo que un adaptador **no** declara es qué acciones requieren confirmación humana: eso no varía por
|
|
18
|
+
runner. Lo fijan los guards de `automatization/hooks/` y las reglas de `planning/rules/system/`, iguales
|
|
19
|
+
para los cuatro.
|
|
10
20
|
|
|
11
21
|
```text
|
|
12
22
|
runners/<nombre>/
|
|
13
23
|
├── README.md
|
|
14
24
|
├── manifest.json
|
|
15
|
-
├── settings.json
|
|
16
|
-
├── archivo de instrucciones (si aplica)
|
|
17
|
-
└── comandos
|
|
25
|
+
├── settings.json | hooks.json configuración del runner, según lo que lea cada uno
|
|
26
|
+
├── archivo de instrucciones CLAUDE.md, GEMINI.md, AGENTS.md (si aplica)
|
|
27
|
+
└── artefactos propios comandos, skills, reglas, puentes (si aplica)
|
|
18
28
|
```
|
|
19
29
|
|
|
20
30
|
Adaptadores incluidos: `claude`, `codex`, `antigravity` y `gemini`. Antigravity es la opción Google
|
|
21
|
-
recomendada para cuentas individuales y proyectos nuevos; Gemini se conserva para Enterprise, Google
|
|
22
|
-
y autenticación mediante API keys.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
recomendada para cuentas individuales y proyectos nuevos; Gemini se conserva para Enterprise, Google
|
|
32
|
+
Cloud y autenticación mediante API keys.
|
|
33
|
+
|
|
34
|
+
Se instalan explícitamente —crear el proyecto no toca la configuración activa del usuario— y se
|
|
35
|
+
comprueban sin autenticar:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node tools/ops.js automation install . <runner>
|
|
39
|
+
node tools/ops.js automation doctor . <runner>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`doctor` verifica configuración, instrucciones, artefactos, cargos y disponibilidad del CLI.
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
# Antigravity CLI
|
|
2
2
|
|
|
3
|
-
Runner Google recomendado para cuentas individuales y proyectos nuevos. Usa el ejecutable `agy` y se
|
|
4
|
-
como plugin nativo de workspace en `.agents/plugins/cauce/`:
|
|
3
|
+
Runner Google recomendado para cuentas individuales y proyectos nuevos. Usa el ejecutable `agy` y se
|
|
4
|
+
instala como plugin nativo de workspace en `.agents/plugins/cauce/`:
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
node tools/ops.js automation install . antigravity
|
|
8
|
+
node tools/ops.js automation doctor . antigravity
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
`automation install` deja los archivos y no autentica. El paso que los pone a correr es
|
|
12
|
+
`agy plugin install .agents/plugins/cauce`, que el instalador imprime cuando falta. Ese paso **copia el
|
|
13
|
+
plugin a `~/.gemini/config/plugins/cauce/` y es esa copia la que `agy` ejecuta**, con un registro por
|
|
14
|
+
usuario y no por workspace. De ahí salen tres consecuencias que conviene tener presentes:
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
- Las rutas del `hooks.json` se resuelven contra la carpeta del plugin, no contra el workspace.
|
|
17
|
+
- El payload no nombra el workspace —manda `workspacePaths` vacío y un `Cwd` que apunta al scratch del
|
|
18
|
+
CLI o al home—, así que el puente lleva la raíz ops escrita como ruta absoluta al instalar.
|
|
19
|
+
- Registrar desde otro proyecto reemplaza el plugin del anterior.
|
|
20
|
+
|
|
21
|
+
Volvé a registrar cada vez que `automation install` cambie el wiring o el proyecto se mueva de lugar;
|
|
22
|
+
`agy plugin validate .agents/plugins/cauce` comprueba la copia del repo antes de registrarla.
|
|
23
|
+
|
|
24
|
+
El plugin aporta hooks `PreToolUse` y `Stop`, reglas Cauce y los cinco recorridos —`/cauce:onboard`,
|
|
25
|
+
`/cauce:team`, `/cauce:autobuild`, `/cauce:integration-sync` y `/cauce:integration-promote`— más el
|
|
26
|
+
catálogo de cargos como skills. El bridge convierte el contrato JSON camelCase de Antigravity al motor
|
|
27
|
+
compartido de guards y devuelve decisiones nativas `allow`, `deny` o `continue`.
|
|
28
|
+
|
|
29
|
+
Antigravity también lee el `AGENTS.md` del workspace. El runner `gemini` permanece disponible por
|
|
30
|
+
separado para Enterprise, Google Cloud y API keys.
|
|
@@ -17,24 +17,59 @@ function readInput() {
|
|
|
17
17
|
// no hallaba `ops.config.json` y, como falla cerrado, negaba cada llamada a herramienta.
|
|
18
18
|
const OPS_DIR = '{{OPS_DIR}}'
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Y la ruta absoluta, porque con Antigravity no hay de dónde deducirla. Su payload manda
|
|
21
|
+
// `workspacePaths` vacío y un `Cwd` que apunta al scratch del CLI o al home; el hook lo ejecuta `agy`
|
|
22
|
+
// desde la copia que registró en `~/.gemini/config/plugins/`, cuyo `__dirname` no lleva a ningún
|
|
23
|
+
// proyecto. Nada de eso nombra el workspace, así que el ancla se escribe al instalar o no existe.
|
|
24
|
+
const OPS_ROOT = '{{OPS_ROOT}}'
|
|
25
|
+
|
|
26
|
+
// Los dos marcadores y la carpeta desde la que corre el puente, juntos y pasables como argumento. El
|
|
27
|
+
// default es lo que `automation install` deja escrito; poder reemplazarlo es lo que permite ejercer
|
|
28
|
+
// desde el repositorio lo que sólo existe instalado. Sin eso, la resolución de la raíz —donde ya se
|
|
29
|
+
// escondieron dos fallas que negaban cada llamada a herramienta— sólo se puede probar sobre una copia,
|
|
30
|
+
// y una copia no la mide ninguna cobertura.
|
|
31
|
+
const MARCAS = { dir: OPS_DIR, root: OPS_ROOT, plugin: __dirname }
|
|
32
|
+
|
|
33
|
+
function isRoot(dir) {
|
|
34
|
+
const instance = fs.existsSync(path.join(dir, 'planning'))
|
|
35
|
+
const toolkit = fs.existsSync(path.join(dir, 'engine', 'hooks', 'run.js'))
|
|
36
|
+
return fs.existsSync(path.join(dir, 'ops.config.json')) && (instance || toolkit)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function declaredRoot(marcas) {
|
|
40
|
+
if (!marcas.root.startsWith('{{') && isRoot(marcas.root)) return marcas.root
|
|
41
|
+
// El plugin corriendo desde donde `automation install` lo dejó, que es el caso sin registrar.
|
|
42
|
+
const installed = path.resolve(marcas.plugin, '..', '..', '..')
|
|
43
|
+
const root = marcas.dir.startsWith('{{') ? installed : path.join(installed, marcas.dir)
|
|
24
44
|
return fs.existsSync(path.join(root, 'ops.config.json')) ? root : ''
|
|
25
45
|
}
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
// En sidecar se abre la carpeta de la compañía y la raíz ops es una de sus hijas, así que buscar sólo
|
|
48
|
+
// hacia arriba no la encuentra nunca: el puente fallaba cerrado y negaba cada llamada a herramienta.
|
|
49
|
+
// Un nivel hacia abajo alcanza para los dos modos y es determinista; recorrer el árbol del producto,
|
|
50
|
+
// no. Dos candidatas hermanas es una ambigüedad que nadie puede resolver acá: se abstiene.
|
|
51
|
+
function childRoot(dir) {
|
|
52
|
+
let entries = []
|
|
53
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return '' }
|
|
54
|
+
const roots = entries
|
|
55
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
56
|
+
.map((entry) => path.join(dir, entry.name))
|
|
57
|
+
.filter(isRoot)
|
|
58
|
+
return roots.length === 1 ? roots[0] : ''
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findRoot(input, marcas = MARCAS) {
|
|
62
|
+
const declared = declaredRoot(marcas)
|
|
29
63
|
if (declared) return declared
|
|
30
64
|
const args = input.toolCall && input.toolCall.args || {}
|
|
31
65
|
const starts = [args.Cwd, process.cwd(), ...(input.workspacePaths || [])].filter(Boolean)
|
|
32
66
|
for (const start of starts) {
|
|
33
|
-
|
|
67
|
+
const base = path.resolve(start)
|
|
68
|
+
const child = isRoot(base) ? base : childRoot(base)
|
|
69
|
+
if (child) return child
|
|
70
|
+
let current = path.dirname(base)
|
|
34
71
|
while (true) {
|
|
35
|
-
|
|
36
|
-
const toolkit = fs.existsSync(path.join(current, 'engine', 'hooks', 'run.js'))
|
|
37
|
-
if (fs.existsSync(path.join(current, 'ops.config.json')) && (instance || toolkit)) return current
|
|
72
|
+
if (isRoot(current)) return current
|
|
38
73
|
const parent = path.dirname(current)
|
|
39
74
|
if (parent === current) break
|
|
40
75
|
current = parent
|
|
@@ -56,14 +91,38 @@ function runtimeAt(root) {
|
|
|
56
91
|
return require(runtime)
|
|
57
92
|
}
|
|
58
93
|
|
|
59
|
-
|
|
94
|
+
// La carpeta que el runner abrió, deducida de la raíz: en sidecar la raíz ops es su hija, y en modo
|
|
95
|
+
// embebido son la misma.
|
|
96
|
+
function workspaceOf(root, marcas) {
|
|
97
|
+
const relative = marcas.dir.startsWith('{{') ? '' : marcas.dir.replace(/\/+$/, '')
|
|
98
|
+
if (!relative) return root
|
|
99
|
+
return path.resolve(root, ...relative.split('/').map(() => '..'))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function within(dir, base) {
|
|
103
|
+
const relative = path.relative(base, dir)
|
|
104
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Contra qué resuelven los guards una ruta relativa o el directorio git. El `Cwd` de Antigravity no
|
|
108
|
+
// sirve para eso: apunta al scratch del CLI o al home, así que un guard que juzgue `src/x.js` estaría
|
|
109
|
+
// juzgando otro archivo, y uno que busque el repo git lo buscaría fuera del proyecto. Se respeta el
|
|
110
|
+
// que manda sólo si cae adentro del workspace —si algún día manda uno real, es mejor que el nuestro—;
|
|
111
|
+
// si no, el workspace, que es donde el runner dice estar trabajando.
|
|
112
|
+
function cwdFor(args, root, marcas) {
|
|
113
|
+
const declared = args.Cwd && path.resolve(String(args.Cwd))
|
|
114
|
+
const workspace = workspaceOf(root, marcas)
|
|
115
|
+
return declared && within(declared, workspace) ? declared : workspace
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalize(input, root, marcas = MARCAS) {
|
|
60
119
|
const args = input.toolCall && input.toolCall.args || {}
|
|
61
120
|
const file = args.TargetFile || args.AbsolutePath || ''
|
|
62
121
|
const content = args.CodeContent || args.ReplacementContent
|
|
63
122
|
|| (args.ReplacementChunks && JSON.stringify(args.ReplacementChunks)) || ''
|
|
64
123
|
return {
|
|
65
124
|
sessionId: input.conversationId,
|
|
66
|
-
cwd: args
|
|
125
|
+
cwd: cwdFor(args, root, marcas),
|
|
67
126
|
tool_input: {
|
|
68
127
|
command: args.CommandLine || '',
|
|
69
128
|
file_path: file,
|
|
@@ -81,13 +140,18 @@ function evaluate(event, input) {
|
|
|
81
140
|
const root = findRoot(input)
|
|
82
141
|
process.env.OPS_ROOT = root
|
|
83
142
|
const hooks = runtimeAt(root)
|
|
84
|
-
const normalized = normalize(input)
|
|
143
|
+
const normalized = normalize(input, root)
|
|
85
144
|
if (!hooks.hookGroups[event]) throw new Error(`Evento Antigravity desconocido: ${event || '(vacío)'}`)
|
|
86
145
|
hooks.executeAll([event], normalized)
|
|
87
146
|
return event === 'stop' ? { decision: 'stop' } : { decision: 'allow' }
|
|
88
147
|
} catch (error) {
|
|
89
|
-
|
|
90
|
-
return { decision: 'deny', reason
|
|
148
|
+
const reason = `Cauce: ${error.message}`
|
|
149
|
+
if (event !== 'stop') return { decision: 'deny', reason }
|
|
150
|
+
// Un guard que bloquea marca su error con `blocked` (engine/hooks/run.js); cualquier otro es que el
|
|
151
|
+
// puente no llegó a juzgar nada. En `stop` los dos devolvían `continue`, y eso ata al agente: la
|
|
152
|
+
// raíz que no resuelve no se arregla sola, así que cada intento de cerrar repite el mismo error.
|
|
153
|
+
// El bloqueo sigue dando `continue` —es el mecanismo funcionando—; la falla deja cerrar y avisa.
|
|
154
|
+
return error.blocked ? { decision: 'continue', reason } : { decision: 'stop', reason }
|
|
91
155
|
}
|
|
92
156
|
}
|
|
93
157
|
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
{
|
|
5
5
|
"matcher": "run_command",
|
|
6
6
|
"hooks": [
|
|
7
|
-
{ "type": "command", "command": "node
|
|
7
|
+
{ "type": "command", "command": "node hook.js pre-shell", "timeout": 120 }
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"matcher": "write_to_file|replace_file_content|multi_replace_file_content",
|
|
12
12
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "node
|
|
13
|
+
{ "type": "command", "command": "node hook.js pre-files", "timeout": 30 }
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
17
|
"Stop": [
|
|
18
|
-
{ "type": "command", "command": "node
|
|
18
|
+
{ "type": "command", "command": "node hook.js stop", "timeout": 120 }
|
|
19
19
|
]
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "antigravity",
|
|
4
4
|
"command": "agy",
|
|
5
|
-
"lifecycle": {
|
|
6
|
-
"recommendedForNewProjects": true,
|
|
7
|
-
"audience": "individual-and-new-google-projects"
|
|
8
|
-
},
|
|
9
5
|
"config": {
|
|
10
6
|
"source": "hooks.json",
|
|
11
|
-
"target": ".agents/plugins/cauce/hooks.json"
|
|
7
|
+
"target": ".agents/plugins/cauce/hooks.json",
|
|
8
|
+
"owned": true
|
|
12
9
|
},
|
|
13
10
|
"instructions": [],
|
|
14
11
|
"artifacts": [
|
|
@@ -62,7 +59,7 @@
|
|
|
62
59
|
"expect": "cauce"
|
|
63
60
|
},
|
|
64
61
|
"commands": {
|
|
65
|
-
"invocation": "cauce:{name}",
|
|
62
|
+
"invocation": "/cauce:{name}",
|
|
66
63
|
"names": [
|
|
67
64
|
"onboard",
|
|
68
65
|
"team",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Cauce
|
|
2
2
|
|
|
3
|
-
Lee y cumple `AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/rules/system/` antes
|
|
3
|
+
Lee y cumple `{{OPS_DIR}}AGENTS.md`, `{{OPS_DIR}}planning/PROTOCOL.md` y `{{OPS_DIR}}planning/rules/system/` antes
|
|
4
4
|
de ejecutar trabajo. `{{OPS_DIR}}planning/WIP.md` es el mutex de
|
|
5
5
|
ejecución y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
|
|
6
6
|
inventes aprobaciones o credenciales y no hagas push ni deploy. Cierra cada tarea con verificación real y
|
|
@@ -3,72 +3,4 @@ name: onboard
|
|
|
3
3
|
description: Escanea el repositorio y deja escrito el contexto de la empresa y la primera épica.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
este proyecto.
|
|
8
|
-
|
|
9
|
-
Empezá por `node {{OPS_DIR}}tools/ops.js onboard`, que es instantáneo y te dice tres cosas: si la
|
|
10
|
-
instancia sigue vacía, qué hay en el workspace y con qué pregunta empezar. Si ya tiene contexto escrito,
|
|
11
|
-
no la pises: reescribir lo que alguien corrigió no deja rastro de lo que se perdió.
|
|
12
|
-
|
|
13
|
-
La conversación empieza por una sola pregunta —de qué trata el proyecto, la primera línea que ese comando
|
|
14
|
-
imprime— y la hacés antes de mirar el inventario, sea el workspace vacío, un monorepo o diez repos.
|
|
15
|
-
Sigue según lo que conteste: hasta tres más, formuladas con las palabras de ese proyecto, hasta cubrir
|
|
16
|
-
las dimensiones que el comando haya listado. Una por vez, esperando respuesta. El inventario ya viene
|
|
17
|
-
resuelto: no recorras el árbol ni leas código para completarlo. No des por sentado que vende algo: puede sostenerse con
|
|
18
|
-
donaciones, presupuesto interno o trabajo voluntario, y preguntarle a un proyecto libre quién le paga es
|
|
19
|
-
empezar por una respuesta que nadie dio.
|
|
20
|
-
|
|
21
|
-
El inventario no lo hagas a mano: `node {{OPS_DIR}}tools/ops.js scan --json` devuelve los subproyectos con
|
|
22
|
-
manifiesto propio, su runtime y los comandos que cada uno declara, con el archivo del que salieron.
|
|
23
|
-
Recorrer directorios es determinista y cuesta milisegundos; explorarlo vos cuesta minutos y encuentra lo
|
|
24
|
-
mismo. No corras ningún comando del proyecto: el mapa dice lo que está declarado y de dónde, y
|
|
25
|
-
verificarlo corriéndolo es una historia de la épica, con dueño y tiempo asignado.
|
|
26
|
-
|
|
27
|
-
Con eso escribí `{{OPS_DIR}}organization/company.md` y `product.md`, la sección «Mapa real» de
|
|
28
|
-
`{{OPS_DIR}}AGENTS.md` con el resultado que obtuviste por comando, y las raíces reales en
|
|
29
|
-
`workspaceRoots` de `{{OPS_DIR}}ops.config.json`, que es lo que un guard usa para bloquear una escritura
|
|
30
|
-
fuera de lugar. Lo deducido va marcado `(supuesto)` y lo que nada sostiene queda «Por definir»: no
|
|
31
|
-
inventes clientes, ingresos ni objetivos.
|
|
32
|
-
|
|
33
|
-
Credenciales, MCP y el permiso de push no te corresponden. Cada uno va como fila en
|
|
34
|
-
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` con la acción concreta que lo desbloquea y sin proponer ningún
|
|
35
|
-
valor; las preguntas abiertas, a la sección Ideas de `{{OPS_DIR}}planning/INBOX.md`.
|
|
36
|
-
|
|
37
|
-
El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, dejar la instancia correcta
|
|
38
|
-
para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
|
|
39
|
-
algo concreto.
|
|
40
|
-
|
|
41
|
-
Cerrá escribiendo la épica en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
|
|
42
|
-
atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
|
|
43
|
-
comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—.
|
|
44
|
-
**Nunca promuevas al BACKLOG**: esa firma es humana.
|
|
45
|
-
|
|
46
|
-
## Antes de decir que terminaste
|
|
47
|
-
|
|
48
|
-
Esta lista no es un resumen de lo de arriba: es lo que se comprueba mirando el disco. Una corrida real
|
|
49
|
-
falló los cinco puntos sin darse cuenta, y todos del mismo lado —lo que no produce un archivo visible—.
|
|
50
|
-
|
|
51
|
-
1. **Una pregunta por vez.** Si mandaste dos o más juntas, numeradas, hiciste un formulario. La segunda
|
|
52
|
-
pregunta depende de la primera respuesta: por eso se hacen de a una.
|
|
53
|
-
2. **`epic-NNN-<slug>.md`**, con slug en el nombre y `status: open`. `epic-001.md` no lo lee nadie: ni
|
|
54
|
-
`check`, ni `tree`, ni el runner que busca trabajo. Existe y no existe a la vez.
|
|
55
|
-
3. **Las secciones de `{{OPS_DIR}}organization/` son las del molde.** Escribí adentro de ellas; agregá las tuyas
|
|
56
|
-
abajo si hacen falta. Reemplazar la estructura pierde dimensiones que nadie va a reclamar después,
|
|
57
|
-
porque el archivo se lee completo.
|
|
58
|
-
4. **`HUMAN_ACTIONS.md` tiene filas.** Una por credencial nombrada en el inventario, una por sistema
|
|
59
|
-
externo o MCP, una por la autoridad de push. Si quedó vacío, no es que no hubiera nada: es que lo que
|
|
60
|
-
no te corresponde se perdió en vez de quedar escrito para alguien.
|
|
61
|
-
5. **`ops` no es un servicio del producto.** No va en el mapa real: es la instancia desde la que trabajás.
|
|
62
|
-
|
|
63
|
-
6. **Ninguna dimensión se completa sin haberla preguntado.** Si llegaste al tope de preguntas y una
|
|
64
|
-
quedó afuera —quién lo usa, qué está muerto, qué externo hay— va «Por definir» con su pregunta en
|
|
65
|
-
`{{OPS_DIR}}planning/INBOX.md`, aunque puedas imaginar la respuesta. Y lo que deducís de otra
|
|
66
|
-
respuesta va marcado `(supuesto)`, por plausible que sea: sin la marca se lee con el mismo peso que
|
|
67
|
-
lo que la persona dijo, y nadie va a volver a preguntarlo.
|
|
68
|
-
7. **Corré `node {{OPS_DIR}}tools/ops.js onboard`.** Si te vuelve a ofrecer la pregunta de apertura, la
|
|
69
|
-
instancia sigue vacía: no escribiste nada, y no hay nada que informar. Una corrida real entregó un
|
|
70
|
-
resumen en pasado —«registrado», «documentado», «creada la primera épica»— sobre archivos que en el
|
|
71
|
-
disco seguían siendo el molde. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
|
|
72
|
-
|
|
73
|
-
Recién ahí, `node {{OPS_DIR}}tools/ops.js check planning`. Si sale con advertencias, leelas: son
|
|
74
|
-
exactamente estas cosas.
|
|
6
|
+
{{INCLUDE:shared/onboard.md}}
|
|
@@ -6,8 +6,14 @@ Adaptador nativo mediante `PreToolUse` y `Stop`. Instalar con:
|
|
|
6
6
|
node tools/ops.js automation install . claude
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves. Si ya
|
|
10
|
+
existe una versión distinta de un archivo, se detiene sin sobrescribirla para no destruir
|
|
11
|
+
personalizaciones del proyecto. También crea `CLAUDE.md` cuando no existe y conserva uno existente.
|
|
12
|
+
|
|
13
|
+
En `.claude/workflows/` deja los cinco recorridos que anuncia —`/onboard`, `/team`, `/autobuild`,
|
|
14
|
+
`/integration-sync` e `/integration-promote`— y tres del ciclo de cargos que se invocan igual pero no
|
|
15
|
+
figuran en la lista de recorridos: `/agent-eval`, `/agent-propose` y `/agent-promote`. El catálogo de
|
|
16
|
+
cargos llega como skills en `.claude/skills/`.
|
|
17
|
+
|
|
18
|
+
`manifest.json` declara destinos y capacidades. Comprueba todo con
|
|
19
|
+
`node tools/ops.js automation doctor . claude`.
|
|
@@ -7,7 +7,7 @@ antes de trabajar —los tres, no cuando algo sale mal—: acá sólo está lo e
|
|
|
7
7
|
> Codex lee el `AGENTS.md` de la raíz, que es un nombre compartido entre herramientas. Cuando el repo
|
|
8
8
|
> ops **es** la raíz, este archivo no se instala: el `AGENTS.md` de la empresa ya está ahí y manda.
|
|
9
9
|
|
|
10
|
-
Los hooks de `.codex/hooks
|
|
10
|
+
Los hooks de `.codex/hooks.json` son obligatorios y bloquean por su cuenta. Codex no ejecuta los
|
|
11
11
|
workflows JS de Claude —son referencia, no un runtime compatible—, así que el recorrido se hace fase por
|
|
12
12
|
fase siguiendo el protocolo.
|
|
13
13
|
|
|
@@ -33,35 +33,7 @@ ese cargo debe saber de esta empresa está en `{{OPS_DIR}}organization/roles/<sl
|
|
|
33
33
|
|
|
34
34
|
## El arranque
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
|
|
38
|
-
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
39
|
-
|
|
40
|
-
Empezá por `node {{OPS_DIR}}tools/ops.js onboard`, que es instantáneo: la primera línea que imprime es la
|
|
41
|
-
pregunta con la que tenés que abrir —de qué trata el proyecto—, y después vienen el inventario y las
|
|
42
|
-
dimensiones. Hacé esa pregunta tal cual antes de mirar nada, sea el workspace vacío, un monorepo o diez
|
|
43
|
-
repos, y según lo que conteste formulá hasta tres más con las palabras de ese proyecto, una por vez. El
|
|
44
|
-
inventario ya viene resuelto ahí: no recorras el árbol ni leas código para completarlo. No des por
|
|
45
|
-
sentado que vende algo: puede sostenerse con donaciones, presupuesto interno o trabajo voluntario. Con eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de
|
|
46
|
-
`{{OPS_DIR}}AGENTS.md` con cada comando tal como está declarado y de qué archivo salió —sin correrlo—, y
|
|
47
|
-
las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
48
|
-
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
49
|
-
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
50
|
-
lo que falta. Nunca la promuevas.
|
|
51
|
-
|
|
52
|
-
El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, dejar la instancia correcta
|
|
53
|
-
para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
|
|
54
|
-
algo concreto.
|
|
55
|
-
|
|
56
|
-
Antes de darlo por terminado, comprobá cinco cosas mirando el disco: preguntaste de a una y no en
|
|
57
|
-
formulario; la épica se llama `epic-NNN-<slug>.md` con `status: open` —`epic-001.md` no lo lee nadie—;
|
|
58
|
-
las secciones de `{{OPS_DIR}}organization/` siguen siendo las del molde y lo tuyo se agregó adentro;
|
|
59
|
-
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` tiene una fila por credencial, por externo y por la autoridad de
|
|
60
|
-
push; y `ops` no figura como servicio del producto en el mapa. una dimensión que no
|
|
61
|
-
llegaste a preguntar va «Por definir» con su pregunta en `{{OPS_DIR}}planning/INBOX.md` en vez de
|
|
62
|
-
completarse deduciéndola, y lo que deducís de otra respuesta va marcado `(supuesto)`. Cerrá corriendo
|
|
63
|
-
`node {{OPS_DIR}}tools/ops.js onboard`: si te vuelve a ofrecer la pregunta de apertura, no escribiste
|
|
64
|
-
nada y no hay nada que informar. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
|
|
36
|
+
{{INCLUDE:shared/onboard.md}}
|
|
65
37
|
|
|
66
38
|
## Los equipos
|
|
67
39
|
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
# Codex CLI
|
|
2
2
|
|
|
3
|
-
Codex CLI
|
|
4
|
-
configuración del proyecto con:
|
|
3
|
+
Codex CLI expone hooks nativos. Instalar la configuración del proyecto con:
|
|
5
4
|
|
|
6
5
|
```bash
|
|
7
6
|
node tools/ops.js automation install . codex
|
|
8
7
|
```
|
|
9
8
|
|
|
10
|
-
El archivo se instala en `.codex/hooks
|
|
11
|
-
|
|
9
|
+
El archivo se instala en `.codex/hooks.json`. Es una de las cuatro ubicaciones que Codex lee —las otras
|
|
10
|
+
son `.codex/config.toml` y las dos equivalentes bajo `~/.codex/`—; la forma `hooks/hooks.json` es la que
|
|
11
|
+
usa un plugin y no la que lee un repositorio.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
**Instalar no alcanza.** Un hook no gestionado queda registrado pero **no corre** hasta que se lo confía:
|
|
14
|
+
Codex guarda la confianza contra el hash del archivo y saltea en silencio lo nuevo o lo modificado. Abrí
|
|
15
|
+
una sesión y usá `/hooks` para revisarlos y confiarlos; hay que repetirlo cada vez que el wiring cambie,
|
|
16
|
+
porque cambia el hash. No se usa `--dangerously-bypass-hook-trust`.
|
|
17
|
+
|
|
18
|
+
Los `matcher` filtran el **nombre de la herramienta**: los comandos de shell llegan como `Bash` y las
|
|
19
|
+
ediciones como `apply_patch`, `Edit` o `Write`. No son los nombres internos del protocolo.
|
|
20
|
+
|
|
21
|
+
Si actualizás una instalación anterior a este cambio, `.codex/hooks/hooks.json` queda huérfano —Codex
|
|
22
|
+
nunca lo leyó— y se borra a mano.
|
|
23
|
+
|
|
24
|
+
Codex carga las instrucciones de proyecto desde el `AGENTS.md` canónico, por lo que no se instala una
|
|
25
|
+
copia adicional. `manifest.json` declara esta capacidad y `node tools/ops.js automation doctor . codex`
|
|
26
|
+
valida el wiring. Los workflows JS de Claude no se presentan como compatibles con Codex.
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "
|
|
5
|
+
"matcher": "Bash",
|
|
6
6
|
"hooks": [
|
|
7
|
-
{ "type": "command", "command": "{{
|
|
7
|
+
{ "type": "command", "command": "{{OPS_ROOT}}/automatization/hooks/guard-shell.sh" }
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
|
-
"matcher": "apply_patch|
|
|
11
|
+
"matcher": "apply_patch|Edit|Write",
|
|
12
12
|
"hooks": [
|
|
13
|
-
{ "type": "command", "command": "{{
|
|
13
|
+
{ "type": "command", "command": "{{OPS_ROOT}}/automatization/hooks/guard-files.sh" }
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
17
|
"SessionEnd": [
|
|
18
18
|
{
|
|
19
19
|
"hooks": [
|
|
20
|
-
{ "type": "command", "command": "{{
|
|
20
|
+
{ "type": "command", "command": "{{OPS_ROOT}}/automatization/hooks/guard-planning-drift.sh" }
|
|
21
21
|
]
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
@{{OPS_DIR}}planning/rules/system/commits.md
|
|
8
8
|
@{{OPS_DIR}}planning/rules/system/conduct.md
|
|
9
9
|
|
|
10
|
-
`{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/
|
|
10
|
+
`{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/cauce:autobuild` fase por fase; los
|
|
11
11
|
workflows JS de Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/WIP.md` es el mutex
|
|
12
12
|
y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
|
|
13
13
|
|
|
@@ -27,34 +27,18 @@ node {{OPS_DIR}}tools/ops.js team list
|
|
|
27
27
|
## El arranque
|
|
28
28
|
|
|
29
29
|
En una instancia recién creada nadie le explicó todavía al toolkit qué es este proyecto:
|
|
30
|
-
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una
|
|
31
|
-
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
30
|
+
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una
|
|
31
|
+
vez: reescribir un contexto que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
32
32
|
|
|
33
33
|
Empezá por `node {{OPS_DIR}}tools/ops.js onboard`, que es instantáneo: la primera línea que imprime es la
|
|
34
|
-
pregunta con la que tenés que abrir —de qué trata el proyecto
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sentado que vende algo: puede sostenerse con donaciones, presupuesto interno o trabajo voluntario. Con eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de
|
|
39
|
-
`{{OPS_DIR}}AGENTS.md` con cada comando tal como está declarado y de qué archivo salió —sin correrlo—, y
|
|
40
|
-
las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
41
|
-
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
42
|
-
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
43
|
-
lo que falta. Nunca la promuevas.
|
|
34
|
+
pregunta con la que tenés que abrir —de qué trata el proyecto—. Hacésela tal cual y esperá la respuesta
|
|
35
|
+
antes de mirar el inventario, sea el workspace vacío, un monorepo o diez repos. Con lo que te conteste
|
|
36
|
+
invocá `/cauce:onboard`, que lleva el recorrido entero y la lista de lo que se comprueba al final;
|
|
37
|
+
invocarlo antes sólo devuelve la misma pregunta más caro.
|
|
44
38
|
|
|
45
39
|
El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, dejar la instancia correcta
|
|
46
40
|
para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
|
|
47
|
-
algo concreto.
|
|
48
|
-
|
|
49
|
-
Antes de darlo por terminado, comprobá cinco cosas mirando el disco: preguntaste de a una y no en
|
|
50
|
-
formulario; la épica se llama `epic-NNN-<slug>.md` con `status: open` —`epic-001.md` no lo lee nadie—;
|
|
51
|
-
las secciones de `{{OPS_DIR}}organization/` siguen siendo las del molde y lo tuyo se agregó adentro;
|
|
52
|
-
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` tiene una fila por credencial, por externo y por la autoridad de
|
|
53
|
-
push; y `ops` no figura como servicio del producto en el mapa. una dimensión que no
|
|
54
|
-
llegaste a preguntar va «Por definir» con su pregunta en `{{OPS_DIR}}planning/INBOX.md` en vez de
|
|
55
|
-
completarse deduciéndola, y lo que deducís de otra respuesta va marcado `(supuesto)`. Cerrá corriendo
|
|
56
|
-
`node {{OPS_DIR}}tools/ops.js onboard`: si te vuelve a ofrecer la pregunta de apertura, no escribiste
|
|
57
|
-
nada y no hay nada que informar. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
|
|
41
|
+
algo concreto. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
|
|
58
42
|
|
|
59
43
|
Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
|
|
60
44
|
trabajo desde INBOX.
|
|
@@ -10,13 +10,15 @@ node tools/ops.js automation install . gemini
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
Instala `.gemini/settings.json`, el contexto raíz `GEMINI.md` y los comandos `/cauce:onboard`,
|
|
13
|
-
`/cauce:team`, `/cauce:autobuild`, `/cauce:integration-sync` y `/cauce:integration-promote
|
|
13
|
+
`/cauce:team`, `/cauce:autobuild`, `/cauce:integration-sync` y `/cauce:integration-promote`, más el
|
|
14
|
+
catálogo de cargos como skills en `.gemini/skills/`.
|
|
14
15
|
|
|
15
16
|
Antes vivían bajo `/ops:` y eran tres: el arranque y el recorrido de equipo le llegaban sólo como prosa,
|
|
16
17
|
así que alguien que venía de otro runner los buscaba en la lista y no estaban. Si actualizás una
|
|
17
18
|
instalación vieja, `.gemini/commands/ops/` queda huérfano y se borra a mano.
|
|
18
19
|
|
|
19
|
-
Gemini
|
|
20
|
-
|
|
20
|
+
Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool` y `AfterAgent` en
|
|
21
|
+
`.gemini/settings.json`, declarados en `manifest.json`. Sólo corren si la carpeta está marcada como
|
|
22
|
+
confiable —`GEMINI.md` explica qué avisa Gemini cuando no lo está—.
|
|
21
23
|
|
|
22
24
|
Comprueba la instalación con `node tools/ops.js automation doctor . gemini`.
|
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
description = "Arranca una instancia recién creada: pregunta, escribe el contexto y la primera épica"
|
|
2
2
|
prompt = """
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Empezá por `node {{OPS_DIR}}tools/ops.js onboard`: es instantáneo, y la primera línea que imprime es la
|
|
6
|
-
pregunta con la que tenés que abrir. Hacésela tal cual y esperá la respuesta antes de mirar el
|
|
7
|
-
inventario, sea el workspace vacío, un monolito o diez repos. Según lo que conteste, hasta tres
|
|
8
|
-
preguntas más con las palabras de ese proyecto, una por vez. El inventario ya viene resuelto en esa
|
|
9
|
-
salida —servicios, comandos declarados y las variables que cada uno espera—: no recorras el árbol ni
|
|
10
|
-
corras comandos del proyecto para completarlo.
|
|
11
|
-
|
|
12
|
-
Con eso escribí {{OPS_DIR}}organization/, la sección «Mapa real» de {{OPS_DIR}}AGENTS.md con cada
|
|
13
|
-
comando tal como está declarado y de qué archivo salió, y las raíces reales en workspaceRoots. Las
|
|
14
|
-
credenciales, los sistemas externos y el permiso de push van como filas de
|
|
15
|
-
{{OPS_DIR}}planning/HUMAN_ACTIONS.md, sin proponer ningún valor. Una dimensión que no llegaste a
|
|
16
|
-
preguntar queda «Por definir» con su pregunta en {{OPS_DIR}}planning/INBOX.md, y lo que deducís de otra
|
|
17
|
-
respuesta va marcado «(supuesto)».
|
|
18
|
-
|
|
19
|
-
Cerrá escribiendo la épica como epic-NNN-<slug>.md con status: open —sin slug no la lee nadie— y corré
|
|
20
|
-
`node {{OPS_DIR}}tools/ops.js check planning`. Nunca promuevas al BACKLOG: esa firma es humana.
|
|
3
|
+
{{INCLUDE:shared/onboard.md}}
|
|
21
4
|
"""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Es el arranque de una instancia recién creada: `init` la instaló, pero nadie le explicó todavía qué es
|
|
2
|
+
este proyecto.
|
|
3
|
+
|
|
4
|
+
Empezá por `node {{OPS_DIR}}tools/ops.js onboard`, que es instantáneo y te dice tres cosas: si la
|
|
5
|
+
instancia sigue vacía, qué hay en el workspace y con qué pregunta empezar. Si ya tiene contexto escrito,
|
|
6
|
+
no la pises: reescribir lo que alguien corrigió no deja rastro de lo que se perdió.
|
|
7
|
+
|
|
8
|
+
La conversación empieza por una sola pregunta —de qué trata el proyecto, la primera línea que ese comando
|
|
9
|
+
imprime— y la hacés antes de mirar el inventario, sea el workspace vacío, un monorepo o diez repos.
|
|
10
|
+
Sigue según lo que conteste: hasta tres más, formuladas con las palabras de ese proyecto, hasta cubrir
|
|
11
|
+
las dimensiones que el comando haya listado. Una por vez, esperando respuesta. El inventario ya viene
|
|
12
|
+
resuelto: no recorras el árbol ni leas código para completarlo. No des por sentado que vende algo: puede sostenerse con
|
|
13
|
+
donaciones, presupuesto interno o trabajo voluntario, y preguntarle a un proyecto libre quién le paga es
|
|
14
|
+
empezar por una respuesta que nadie dio.
|
|
15
|
+
|
|
16
|
+
El inventario no lo hagas a mano: `node {{OPS_DIR}}tools/ops.js scan --json` devuelve los subproyectos con
|
|
17
|
+
manifiesto propio, su runtime y los comandos que cada uno declara, con el archivo del que salieron.
|
|
18
|
+
Recorrer directorios es determinista y cuesta milisegundos; explorarlo vos cuesta minutos y encuentra lo
|
|
19
|
+
mismo. No corras ningún comando del proyecto: el mapa dice lo que está declarado y de dónde, y
|
|
20
|
+
verificarlo corriéndolo es una historia de la épica, con dueño y tiempo asignado.
|
|
21
|
+
|
|
22
|
+
Con eso escribí `{{OPS_DIR}}organization/company.md` y `product.md`, la sección «Mapa real» de
|
|
23
|
+
`{{OPS_DIR}}AGENTS.md` con el resultado que obtuviste por comando, y las raíces reales en
|
|
24
|
+
`workspaceRoots` de `{{OPS_DIR}}ops.config.json`, que es lo que un guard usa para bloquear una escritura
|
|
25
|
+
fuera de lugar. Lo deducido va marcado `(supuesto)` y lo que nada sostiene queda «Por definir»: no
|
|
26
|
+
inventes clientes, ingresos ni objetivos.
|
|
27
|
+
|
|
28
|
+
Credenciales, MCP y el permiso de push no te corresponden. Cada uno va como fila en
|
|
29
|
+
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` con la acción concreta que lo desbloquea y sin proponer ningún
|
|
30
|
+
valor; las preguntas abiertas, a la sección Ideas de `{{OPS_DIR}}planning/INBOX.md`.
|
|
31
|
+
|
|
32
|
+
El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, dejar la instancia correcta
|
|
33
|
+
para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
|
|
34
|
+
algo concreto.
|
|
35
|
+
|
|
36
|
+
Cerrá escribiendo la épica en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
|
|
37
|
+
atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
|
|
38
|
+
comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—.
|
|
39
|
+
**Nunca promuevas al BACKLOG**: esa firma es humana.
|
|
40
|
+
|
|
41
|
+
## Antes de decir que terminaste
|
|
42
|
+
|
|
43
|
+
Esta lista no es un resumen de lo de arriba: es lo que se comprueba mirando el disco. Una corrida real
|
|
44
|
+
falló los cinco puntos sin darse cuenta, y todos del mismo lado —lo que no produce un archivo visible—.
|
|
45
|
+
|
|
46
|
+
1. **Una pregunta por vez.** Si mandaste dos o más juntas, numeradas, hiciste un formulario. La segunda
|
|
47
|
+
pregunta depende de la primera respuesta: por eso se hacen de a una.
|
|
48
|
+
2. **`epic-NNN-<slug>.md`**, con slug en el nombre y `status: open`. `epic-001.md` no lo lee nadie: ni
|
|
49
|
+
`check`, ni `tree`, ni el runner que busca trabajo. Existe y no existe a la vez.
|
|
50
|
+
3. **Las secciones de `{{OPS_DIR}}organization/` son las del molde.** Escribí adentro de ellas; agregá las tuyas
|
|
51
|
+
abajo si hacen falta. Reemplazar la estructura pierde dimensiones que nadie va a reclamar después,
|
|
52
|
+
porque el archivo se lee completo.
|
|
53
|
+
4. **`HUMAN_ACTIONS.md` tiene filas.** Una por credencial nombrada en el inventario, una por sistema
|
|
54
|
+
externo o MCP, una por la autoridad de push. Si quedó vacío, no es que no hubiera nada: es que lo que
|
|
55
|
+
no te corresponde se perdió en vez de quedar escrito para alguien.
|
|
56
|
+
5. **`ops` no es un servicio del producto.** No va en el mapa real: es la instancia desde la que trabajás.
|
|
57
|
+
|
|
58
|
+
6. **Ninguna dimensión se completa sin haberla preguntado.** Si llegaste al tope de preguntas y una
|
|
59
|
+
quedó afuera —quién lo usa, qué está muerto, qué externo hay— va «Por definir» con su pregunta en
|
|
60
|
+
`{{OPS_DIR}}planning/INBOX.md`, aunque puedas imaginar la respuesta. Y lo que deducís de otra
|
|
61
|
+
respuesta va marcado `(supuesto)`, por plausible que sea: sin la marca se lee con el mismo peso que
|
|
62
|
+
lo que la persona dijo, y nadie va a volver a preguntarlo.
|
|
63
|
+
7. **Corré `node {{OPS_DIR}}tools/ops.js onboard`.** Si te vuelve a ofrecer la pregunta de apertura, la
|
|
64
|
+
instancia sigue vacía: no escribiste nada, y no hay nada que informar. Una corrida real entregó un
|
|
65
|
+
resumen en pasado —«registrado», «documentado», «creada la primera épica»— sobre archivos que en el
|
|
66
|
+
disco seguían siendo el molde. Estar bloqueado es un resultado legítimo; narrarlo como entrega, no.
|
|
67
|
+
|
|
68
|
+
Recién ahí, `node {{OPS_DIR}}tools/ops.js check planning`. Si sale con advertencias, leelas: son
|
|
69
|
+
exactamente estas cosas.
|
|
@@ -64,6 +64,55 @@ function mergeConfig(current, incoming) {
|
|
|
64
64
|
return incoming
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta: `automatization/hooks/`
|
|
68
|
+
// es nuestro y ninguna empresa escribe ahí. Se saca del archivo del usuario antes de fusionar para que
|
|
69
|
+
// el merge deje exactamente las de esta versión, ni una más.
|
|
70
|
+
const ENTREGADO = /automatization\/hooks\/guard-[a-z-]+\.sh/
|
|
71
|
+
|
|
72
|
+
function withoutDeliveredHooks(config, vigentes) {
|
|
73
|
+
const dropped = []
|
|
74
|
+
const walk = (node) => {
|
|
75
|
+
if (Array.isArray(node)) {
|
|
76
|
+
return node
|
|
77
|
+
.filter((item) => {
|
|
78
|
+
const command = item && typeof item === 'object' ? String(item.command || '') : ''
|
|
79
|
+
if (!ENTREGADO.test(command)) return true
|
|
80
|
+
// Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
|
|
81
|
+
// que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
|
|
82
|
+
if (!vigentes.has(command)) dropped.push(command)
|
|
83
|
+
return false
|
|
84
|
+
})
|
|
85
|
+
.map(walk)
|
|
86
|
+
.filter((item) => !(item && typeof item === 'object' && Array.isArray(item.hooks) && !item.hooks.length))
|
|
87
|
+
}
|
|
88
|
+
if (node && typeof node === 'object') {
|
|
89
|
+
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, walk(value)]))
|
|
90
|
+
}
|
|
91
|
+
return node
|
|
92
|
+
}
|
|
93
|
+
return { config: walk(config), dropped }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Por qué se fue cada entrada nuestra. Un guard suelto que ahora cubre un grupo no es lo mismo que una
|
|
97
|
+
// ruta que dejó de existir: el primero se ejecutaba dos veces por herramienta —con `verify`, la suite
|
|
98
|
+
// entera del proyecto dos veces por commit—, y el segundo no se ejecutaba nunca. Decirlo distinto es lo
|
|
99
|
+
// único que le permite a alguien darse cuenta de cuál de los dos tenía.
|
|
100
|
+
function reportarQuitadas(name, dropped, vigentes, output) {
|
|
101
|
+
const wrappers = new Map()
|
|
102
|
+
const sueltas = []
|
|
103
|
+
for (const command of dropped) {
|
|
104
|
+
const hit = supersededGuards().find(
|
|
105
|
+
(entry) => command.endsWith(entry.file) && [...vigentes].some((v) => v.endsWith(entry.wrapper)),
|
|
106
|
+
)
|
|
107
|
+
if (hit) wrappers.set(hit.wrapper, [...(wrappers.get(hit.wrapper) || []), hit.file])
|
|
108
|
+
else sueltas.push(command)
|
|
109
|
+
}
|
|
110
|
+
for (const [wrapper, files] of wrappers) {
|
|
111
|
+
output.log(`− ${name}: reemplazado ${[...new Set(files)].join(', ')} por ${wrapper}`)
|
|
112
|
+
}
|
|
113
|
+
for (const command of sueltas) output.log(`− ${name}: quitada una entrada obsoleta (${command})`)
|
|
114
|
+
}
|
|
115
|
+
|
|
67
116
|
function includesConfig(actual, expected) {
|
|
68
117
|
if (Array.isArray(expected)) {
|
|
69
118
|
return Array.isArray(actual) && expected.every((item) => {
|
|
@@ -79,7 +128,7 @@ function includesConfig(actual, expected) {
|
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
// Dónde abre el dev su herramienta, que no siempre es la raíz ops. En modo sidecar el repo ops es
|
|
82
|
-
// un hermano de los repos de producto:
|
|
131
|
+
// un hermano de los repos de producto: `<empresa>-ops/` coordina, `<empresa>/` es lo que se abre.
|
|
83
132
|
// Instalar dentro del sidecar dejaría al runner sin ver una sola línea de código.
|
|
84
133
|
function installRoot(root) {
|
|
85
134
|
try {
|
|
@@ -89,7 +138,7 @@ function installRoot(root) {
|
|
|
89
138
|
return root
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
// Cómo se nombra la raíz ops desde ahí:
|
|
141
|
+
// Cómo se nombra la raíz ops desde ahí: `<empresa>-ops/` en sidecar, vacío cuando coinciden.
|
|
93
142
|
function opsPrefix(root) {
|
|
94
143
|
const relative = path.relative(installRoot(root), root)
|
|
95
144
|
return relative ? `${relative.split(path.sep).join('/')}/` : ''
|
|
@@ -114,6 +163,8 @@ function runnerPaths(root, name, runner) {
|
|
|
114
163
|
|
|
115
164
|
function resolveItem(paths, root, name, item) {
|
|
116
165
|
return {
|
|
166
|
+
automationRoot: paths.automationRoot,
|
|
167
|
+
opsRoot: root,
|
|
117
168
|
source: F.assertWithin(
|
|
118
169
|
paths.automationRoot,
|
|
119
170
|
path.resolve(paths.sourceDir, item.source),
|
|
@@ -135,12 +186,42 @@ function resolveItem(paths, root, name, item) {
|
|
|
135
186
|
// Un solo render, y `install` escribe exactamente lo que `doctor` compara.
|
|
136
187
|
const OPS_DIR = '{{OPS_DIR}}'
|
|
137
188
|
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
// Un fragmento que varios adaptadores comparten, resuelto contra la raíz de `automatization/`. El
|
|
190
|
+
// arranque es el mismo trabajo en tres formatos —la sección de un `AGENTS.md`, el cuerpo de un
|
|
191
|
+
// `SKILL.md`, el prompt de un `.toml`—, y escrito tres veces hizo lo que hace siempre una copia: dos
|
|
192
|
+
// de ellas anunciaban cinco puntos y enumeraban seis, con el sexto doblado dentro del quinto.
|
|
193
|
+
//
|
|
194
|
+
// El workflow de Claude queda afuera a propósito: es un programa con fases y esquemas, no una prosa
|
|
195
|
+
// enmarcada, así que su arranque no es una copia de éste sino otra cosa.
|
|
196
|
+
//
|
|
197
|
+
// Se resuelve antes que `{{OPS_DIR}}` para que el fragmento también reciba el prefijo, y no anida:
|
|
198
|
+
// lo incluido se copia tal cual.
|
|
199
|
+
const INCLUDE = /\{\{INCLUDE:([^}]+)\}\}/g
|
|
200
|
+
|
|
201
|
+
function inline(text, automationRoot) {
|
|
202
|
+
return text.replace(INCLUDE, (_, relative) => {
|
|
203
|
+
const shared = F.assertWithin(
|
|
204
|
+
automationRoot,
|
|
205
|
+
path.resolve(automationRoot, relative.trim()),
|
|
206
|
+
'INCLUDE',
|
|
207
|
+
)
|
|
208
|
+
return fs.readFileSync(shared, 'utf8').trimEnd()
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// `{{OPS_ROOT}}` es la raíz absoluta. La necesita quien no puede deducirla de dónde lo ejecutaron
|
|
213
|
+
// —el puente de Antigravity—, y por eso no reemplaza a `{{OPS_DIR}}`: una ruta absoluta escrita en un
|
|
214
|
+
// archivo se rompe si el proyecto se mueve, así que la lleva sólo el que se queda sin alternativa.
|
|
215
|
+
const OPS_ROOT = '{{OPS_ROOT}}'
|
|
216
|
+
|
|
217
|
+
function render(file, prefix, automationRoot, opsRoot = '') {
|
|
218
|
+
return inline(fs.readFileSync(file, 'utf8'), automationRoot)
|
|
219
|
+
.split(OPS_ROOT).join(opsRoot)
|
|
220
|
+
.split(OPS_DIR).join(prefix)
|
|
140
221
|
}
|
|
141
222
|
|
|
142
223
|
function runnerConfig(paths, root) {
|
|
143
|
-
return JSON.parse(render(paths.configSource, opsPrefix(root)))
|
|
224
|
+
return JSON.parse(render(paths.configSource, opsPrefix(root), paths.automationRoot, root))
|
|
144
225
|
}
|
|
145
226
|
|
|
146
227
|
// Cargos del catálogo, con el frontmatter que el runner indexa para elegir a quién invocar.
|
|
@@ -181,6 +262,18 @@ function installRoleSkills(root, runner, output) {
|
|
|
181
262
|
const install = installRoot(root)
|
|
182
263
|
const base = F.assertWithin(install, path.resolve(install, runner.roleSkills), `${runner.name}: roleSkills`)
|
|
183
264
|
const roles = roleCatalog(root)
|
|
265
|
+
// Los cargos y los recorridos comparten el espacio de nombres de skills del runner, así que un cargo
|
|
266
|
+
// que se llame como un recorrido lo pisa. Hoy no pasa, y por eso mismo hay que detenerlo acá: el
|
|
267
|
+
// catálogo de una empresa es suyo, nadie le prohíbe un cargo `team`, y el daño sería que `/cauce:team`
|
|
268
|
+
// deje de existir sin que nada falle. Renombrar el cargo es la salida, y sólo la puede tomar alguien.
|
|
269
|
+
const recorridos = new Set((runner.commands && runner.commands.names) || [])
|
|
270
|
+
const chocan = roles.filter((role) => recorridos.has(role.slug)).map((role) => role.slug)
|
|
271
|
+
if (chocan.length) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`${runner.name}: ${chocan.join(', ')} es a la vez un cargo y un recorrido, y comparten `
|
|
274
|
+
+ `${runner.roleSkills}. Renombrá el cargo en agents/roles/ antes de instalar.`,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
184
277
|
for (const role of roles) {
|
|
185
278
|
const file = path.join(base, role.slug, 'SKILL.md')
|
|
186
279
|
F.assertNoSymlinkPath(install, file)
|
|
@@ -329,42 +422,6 @@ function supersededGuards() {
|
|
|
329
422
|
return entries
|
|
330
423
|
}
|
|
331
424
|
|
|
332
|
-
// Reemplaza las entradas sueltas que este mismo toolkit escribió por el grupo que ya las cubre.
|
|
333
|
-
// Sin esto, una instalación previa ejecuta cada guard dos veces por herramienta: con `verify` eso
|
|
334
|
-
// significa correr la suite de tests del proyecto dos veces en cada commit.
|
|
335
|
-
function pruneSupersededHooks(config) {
|
|
336
|
-
const registered = JSON.stringify(config)
|
|
337
|
-
const targets = supersededGuards().filter((entry) => registered.includes(entry.wrapper))
|
|
338
|
-
const replaced = new Map()
|
|
339
|
-
if (!targets.length) return { config, replaced: [] }
|
|
340
|
-
|
|
341
|
-
const isEmptyEntry = (item) => item && typeof item === 'object'
|
|
342
|
-
&& Array.isArray(item.hooks) && !item.hooks.length
|
|
343
|
-
const walk = (node) => {
|
|
344
|
-
if (Array.isArray(node)) {
|
|
345
|
-
const kept = []
|
|
346
|
-
for (const item of node) {
|
|
347
|
-
const command = item && typeof item === 'object' ? String(item.command || '') : ''
|
|
348
|
-
const hit = targets.find((entry) => command.endsWith(entry.file))
|
|
349
|
-
if (hit) {
|
|
350
|
-
replaced.set(hit.wrapper, [...(replaced.get(hit.wrapper) || []), hit.file])
|
|
351
|
-
continue
|
|
352
|
-
}
|
|
353
|
-
const walked = walk(item)
|
|
354
|
-
if (!isEmptyEntry(walked)) kept.push(walked)
|
|
355
|
-
}
|
|
356
|
-
return kept
|
|
357
|
-
}
|
|
358
|
-
if (node && typeof node === 'object') {
|
|
359
|
-
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, walk(value)]))
|
|
360
|
-
}
|
|
361
|
-
return node
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const pruned = walk(config)
|
|
365
|
-
return { config: pruned, replaced: [...replaced].map(([wrapper, files]) => ({ wrapper, files })) }
|
|
366
|
-
}
|
|
367
|
-
|
|
368
425
|
// Wiring heredado: guards que ahora corren agrupados pero siguen registrados uno por uno.
|
|
369
426
|
// Conviven sin romper nada, a costa de ejecutar el guard dos veces por herramienta.
|
|
370
427
|
function legacyGuardWiring(config) {
|
|
@@ -406,8 +463,10 @@ function probeBridge(paths, runner) {
|
|
|
406
463
|
// Cada evento tiene su respuesta sana: `stop` cierra la sesión —eso es funcionar— y el resto deja
|
|
407
464
|
// pasar. El puente falla cerrado, así que `deny` en una llamada inocua es que algo se rompió antes
|
|
408
465
|
// de poder juzgarla, y `continue` es el camino de error del propio `stop`.
|
|
466
|
+
// Y sin `reason`: el puente sólo la manda cuando algo falló, así que un `stop` que la trae es
|
|
467
|
+
// una falla de infraestructura que se dejó cerrar la sesión, no un arranque sano.
|
|
409
468
|
const sana = evento === 'stop' ? 'stop' : 'allow'
|
|
410
|
-
if (respuesta.decision === sana) continue
|
|
469
|
+
if (respuesta.decision === sana && !respuesta.reason) continue
|
|
411
470
|
const salida = (result.stderr || 'sin respuesta').trim().split('\n')[0]
|
|
412
471
|
const motivo = respuesta.reason
|
|
413
472
|
|| (respuesta.decision ? `respondió ${respuesta.decision}` : salida)
|
|
@@ -442,7 +501,7 @@ function doctor(root, name, output = console) {
|
|
|
442
501
|
// que su hash difiere siempre. Comparado como archivo, `doctor` avisaba cuando el bloque estaba bien
|
|
443
502
|
// y callaba cuando alguien lo había borrado, que es exactamente al revés.
|
|
444
503
|
if (esArchivoCompartido(root, resolved.target)) {
|
|
445
|
-
const contenido = render(resolved.source, opsPrefix(root))
|
|
504
|
+
const contenido = render(resolved.source, opsPrefix(root), resolved.automationRoot, resolved.opsRoot)
|
|
446
505
|
if (!fs.existsSync(resolved.target)) errors.push(`falta ${item.target}`)
|
|
447
506
|
else if (!fs.readFileSync(resolved.target, 'utf8').includes(bloqueInicio(name))) {
|
|
448
507
|
errors.push(`${item.target}: no tiene las instrucciones de Cauce; reinstalá el adaptador`)
|
|
@@ -529,7 +588,8 @@ function deliveryKey(name, target) {
|
|
|
529
588
|
function deliveryState(recorded, name, resolved, prefix = '') {
|
|
530
589
|
if (!fs.existsSync(resolved.target)) return 'nuevo'
|
|
531
590
|
const current = M.digest(resolved.target)
|
|
532
|
-
|
|
591
|
+
const esperado = render(resolved.source, prefix, resolved.automationRoot, resolved.opsRoot)
|
|
592
|
+
if (current === M.digestText(esperado)) return 'al día'
|
|
533
593
|
const delivered = recorded[deliveryKey(name, resolved.item.target)]
|
|
534
594
|
return delivered && delivered === current ? 'desactualizado' : 'ajeno'
|
|
535
595
|
}
|
|
@@ -720,11 +780,15 @@ function install(root, name, output = console, options = {}) {
|
|
|
720
780
|
+ 'configuración de tu runner. Si el cambio ya no te sirve, repetí con --force.',
|
|
721
781
|
)
|
|
722
782
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
783
|
+
// Un archivo que existe sólo porque Cauce lo creó se escribe entero. Los `settings.json` de Claude y
|
|
784
|
+
// Gemini son del usuario, así que ahí se fusiona; pero fusionar conserva también lo que pusimos en
|
|
785
|
+
// una versión anterior, y una entrada nuestra que quedó viva apuntando a donde ya no hay nada es un
|
|
786
|
+
// guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
|
|
787
|
+
const vigentes = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
|
|
788
|
+
.map((entry) => JSON.parse(`{${entry}}`).command))
|
|
789
|
+
const limpio = runner.config.owned ? { config: {}, dropped: [] } : withoutDeliveredHooks(current, vigentes)
|
|
790
|
+
reportarQuitadas(name, limpio.dropped, vigentes, output)
|
|
791
|
+
F.atomicWriteJson(paths.configTarget, mergeConfig(limpio.config, incoming))
|
|
728
792
|
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
729
793
|
// el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
|
|
730
794
|
if (paths.install !== root) {
|
|
@@ -736,7 +800,7 @@ function install(root, name, output = console, options = {}) {
|
|
|
736
800
|
const situacion = state.get(resolved)
|
|
737
801
|
const propio = runner.instructions.includes(resolved.item)
|
|
738
802
|
if (propio && esArchivoCompartido(root, resolved.target)) {
|
|
739
|
-
const contenido = render(resolved.source, opsPrefix(root))
|
|
803
|
+
const contenido = render(resolved.source, opsPrefix(root), resolved.automationRoot, resolved.opsRoot)
|
|
740
804
|
if (bloqueAlDia(resolved.target, name, contenido)) {
|
|
741
805
|
output.log(`= ${name}: ${resolved.item.target} ya trae sus instrucciones`)
|
|
742
806
|
} else {
|
|
@@ -752,7 +816,8 @@ function install(root, name, output = console, options = {}) {
|
|
|
752
816
|
output.log(`= ${name}: ${resolved.item.target} ya está al día`)
|
|
753
817
|
} else {
|
|
754
818
|
fs.mkdirSync(path.dirname(resolved.target), { recursive: true })
|
|
755
|
-
|
|
819
|
+
const escrito = render(resolved.source, opsPrefix(root), resolved.automationRoot, resolved.opsRoot)
|
|
820
|
+
F.atomicWrite(resolved.target, escrito)
|
|
756
821
|
const verbo = situacion === 'nuevo' ? 'instalado' : 'actualizado'
|
|
757
822
|
output.log(`✓ ${name}: ${verbo} ${resolved.item.target}`)
|
|
758
823
|
}
|
|
@@ -798,6 +863,7 @@ module.exports = {
|
|
|
798
863
|
legacyGuardWiring,
|
|
799
864
|
roleCatalog,
|
|
800
865
|
roleSkill,
|
|
866
|
+
render,
|
|
801
867
|
listHooks,
|
|
802
868
|
runnerManifest,
|
|
803
869
|
}
|
package/engine/cli/ops.js
CHANGED
|
@@ -1286,7 +1286,8 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
1286
1286
|
const force = cli.has('--force')
|
|
1287
1287
|
try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
|
|
1288
1288
|
if (runnerName === 'codex') {
|
|
1289
|
-
console.log(' Codex
|
|
1289
|
+
console.log(' Codex deja los hooks nuevos sin correr hasta que los confíes: abrí una sesión')
|
|
1290
|
+
console.log(' y usá /hooks para revisarlos y marcarlos como confiables.')
|
|
1290
1291
|
}
|
|
1291
1292
|
if (!runner.capabilities.nativeHooks) {
|
|
1292
1293
|
console.log(` ${runnerName} no expone hooks nativos; aplica guards como prechecks.`)
|
package/engine/hooks/run.js
CHANGED
|
@@ -29,12 +29,23 @@ function fileOf(input) {
|
|
|
29
29
|
|| input.file_path || input.path || process.env.OPS_HOOK_FILE || '')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// El sobre de `apply_patch`, venga por donde venga. Codex lo manda entero como `command` en vez de
|
|
33
|
+
// `patch`, y sin reconocerlo ahí el guard de archivos no ve ni un archivo: mira una escritura que
|
|
34
|
+
// reemplaza una migración o filtra una credencial y la deja pasar sin decir nada. Se exige el
|
|
35
|
+
// encabezado en vez de aceptar cualquier `command`, para no leer un comando de shell como si fuera
|
|
36
|
+
// contenido de archivo.
|
|
37
|
+
function patchOf(input) {
|
|
38
|
+
const campos = input.tool_input || {}
|
|
39
|
+
const command = String(campos.command || '')
|
|
40
|
+
const sobre = command.startsWith('*** Begin Patch') ? command : ''
|
|
41
|
+
return String(campos.patch || campos.input || input.patch || sobre || '')
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function filesOf(input) {
|
|
33
45
|
const files = new Set()
|
|
34
46
|
const direct = fileOf(input)
|
|
35
47
|
if (direct) files.add(direct)
|
|
36
|
-
const patch =
|
|
37
|
-
|| input.patch || '')
|
|
48
|
+
const patch = patchOf(input)
|
|
38
49
|
for (const match of patch.matchAll(/^\*\*\* (?:Add|Update|Delete) File:\s*(.+)$/gm)) files.add(match[1].trim())
|
|
39
50
|
return [...files]
|
|
40
51
|
}
|
|
@@ -46,7 +57,7 @@ function contentOf(input) {
|
|
|
46
57
|
|| input.tool_input.patch
|
|
47
58
|
|| input.tool_input.input
|
|
48
59
|
)
|
|
49
|
-
|| input.content || input.patch || '')
|
|
60
|
+
|| input.content || input.patch || patchOf(input) || '')
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
function cwdOf(input) {
|