@ingeniomaps/cauce 0.29.0 → 0.31.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 CHANGED
@@ -14,6 +14,90 @@ 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.31.0] - 2026-08-18
18
+
19
+ ### Añadido
20
+
21
+ - **`ops automation uninstall <ops-root> <runner>`: sacar el wiring sin llevarse lo tuyo.** Hasta ahora
22
+ desinstalar era borrar la carpeta ops y descubrir después que cada llamada de herramienta ejecuta un
23
+ guard que ya no existe; la otra salida —borrar `.claude/` entero— se lleva puesto lo que hayas puesto
24
+ vos. El comando quita lo que Cauce entregó **y sigue igual que como lo entregó**: sus guards de la
25
+ configuración del runner, sus workflows, sus punteros a cargos. Tus hooks, tus workflows y tus skills
26
+ quedan donde están, y un archivo del toolkit que hayas editado se conserva y aparece nombrado en la
27
+ salida, porque decidir sobre él es tuyo. La instancia no se toca: borrarla es una decisión aparte.
28
+ También como `make uninstall-claude` y sus equivalentes.
29
+
30
+ ### Corregido
31
+
32
+ - **Una instancia dejaba de enterarse de que había versiones nuevas.** `init` fija la versión exacta del
33
+ motor, así que `npm update` no la mueve y `upgrade` compara contra el que está instalado: quien
34
+ actualizaba con `make upgrade` recibía «la instancia está al día» en todas las versiones siguientes, sin
35
+ nada que le dijera que la comparación era local. Ahora la salida nombra contra qué comparó y da el
36
+ comando que trae un motor nuevo, y `make upgrade` lo corre antes de aplicar.
37
+
38
+ Actualizar son tres pasos y el tercero sigue siendo aparte: `npm install --save-dev
39
+ @ingeniomaps/cauce@latest`, `ops upgrade .` y `ops automation install . <runner>`. Los workflows y las
40
+ skills viven en el runner, no en la instancia, y `upgrade` lo recuerda al terminar.
41
+
42
+ ### Cambiado
43
+
44
+ - **`init` termina en un solo cierre, y sus opciones se leen como una selección.** Las tres líneas finales
45
+ —cada una con tono de última— pasaron a ser una con la acción concreta, y las opciones de runner e
46
+ integración van una por línea con el default marcado donde se mira: `5) ninguno ← Enter`, en vez de un
47
+ `[ninguno]` pegado al prompt.
48
+
49
+ ## [0.30.0] - 2026-08-18
50
+
51
+ ### Añadido
52
+
53
+ - **`ops onboard`: qué le falta a una instancia para arrancar, y con qué pregunta empezar.** Determinista
54
+ y en milisegundos: dice si la instancia sigue vacía, qué hay en el workspace y qué queda por cubrir.
55
+ `init` lo imprime al terminar, que es donde mira quien acaba de instalar y todavía no sabe qué hace la
56
+ herramienta. Cuando `organization/` o el roadmap ya tienen contenido real no ofrece nada, porque habría
57
+ trabajo que pisar.
58
+
59
+ ### Cambiado
60
+
61
+ - **El arranque pregunta en vez de negarse, y pregunta primero.** Invocado sin contexto, `/onboard`
62
+ terminaba diciendo «volvé a correrlo con contexto» —y gastaba un subagente para decirlo—. Ahora la
63
+ primera línea es la pregunta, sea el workspace vacío, un monorepo o diez repos, y el inventario viene
64
+ después: de qué trata el proyecto no depende de lo que haya en el disco. Cada runner recibe la
65
+ instrucción de abrir con esa pregunta antes de mirar nada.
66
+
67
+ - **El recorrido tiene techo: una llamada cuando falta contexto, tres cuando hay con qué escribir.** Y
68
+ ninguna sale a explorar —tiene prohibido recorrer directorios, leer código fuente y abrir archivos que
69
+ no vaya a escribir—, porque lo que necesita ya está resuelto: un arranque que te hace esperar diez
70
+ minutos dejó de ser un arranque.
71
+
72
+ - **El escaneo mira las raíces declaradas en `ops.config.json`, y nada por encima.** Antes suponía la
73
+ carpeta madre en modo sidecar; ahora acotar `workspaceRoots` acota también lo que se lee, que es más
74
+ rápido y es lo único que alguien autorizó.
75
+
76
+ - **Y saltea lo que tu proyecto ya declaró basura.** Además de los directorios de paquetes y de los
77
+ ocultos, lee el `.gitignore` de cada raíz y omite los nombres de directorio que encuentre ahí. Los
78
+ interpreta de forma ancha —por nombre, a cualquier profundidad, salteando todo patrón con comodín—:
79
+ alcanza para decidir si vale la pena mirar adentro, y no es un parser de gitignore. Las listas largas
80
+ se recortan a veinte en pantalla diciendo cuántas quedaron afuera; `--json` las trae todas.
81
+
82
+ - **El arranque declara para qué es.** Entender qué es el proyecto, dejar la instancia correcta para él y
83
+ que la primera tarea pueda empezar: eso, y nada más. El análisis profundo llega cuando alguien pide
84
+ algo concreto, y adelantarlo retrasa el único momento en que la herramienta todavía no sirve para nada.
85
+ Está escrito en cada prompt del recorrido y en las instrucciones de los cuatro runners.
86
+
87
+ - **Las preguntas dejan de ser un formulario, y de dar por sentado que el proyecto vende algo.** Antes
88
+ eran cuatro fijas, y la primera preguntaba qué vende la empresa y a quién: a un proyecto libre, interno
89
+ o sin fines de lucro le pedían una respuesta que nadie había dado. Ahora hay una sola pregunta escrita
90
+ —de qué trata el proyecto, la única que no depende de ninguna respuesta— y lo que el motor fija después
91
+ son las dimensiones a cubrir: a quién sirve, cómo se sostiene, qué querés que pase, qué está fuera de
92
+ alcance, qué externos hay que conectar. Quien conduce la conversación las formula con las palabras de
93
+ ese proyecto, una por vez y hasta tres.
94
+
95
+ - **El molde de `organization/company.md` deja de asumir un negocio.** «Modelo de negocio», «quién paga»
96
+ y «fuentes de ingreso» pasan a ser «De qué se trata», «A quién sirve» y «Cómo se sostiene», que nombra
97
+ donación, presupuesto interno y trabajo voluntario junto con la venta. Lo que no aplica se dice, no se
98
+ completa con algo plausible. Afecta a las instancias nuevas: `upgrade` sólo reemplaza `system/`, así
99
+ que el archivo que ya escribiste sigue siendo tuyo.
100
+
17
101
  ## [0.29.0] - 2026-08-18
18
102
 
19
103
  ### Añadido
package/README.md CHANGED
@@ -31,17 +31,44 @@ instala la dependencia, deja el wiring del runner puesto y valida la instancia a
31
31
 
32
32
  ```text
33
33
  ¿Con qué runner vas a trabajar?
34
- 1) claude 2) codex 3) gemini 4) antigravity 5) ninguno
35
- [ninguno] > 1
34
+
35
+ 1) claude
36
+ 2) codex
37
+ 3) gemini
38
+ 4) antigravity
39
+ 5) ninguno ← Enter
40
+
41
+ > 1
36
42
 
37
43
  ¿Habilitar alguna integración?
38
- 1) jira 2) ninguna
39
- [ninguna] >
44
+
45
+ 1) jira
46
+ 2) ninguna ← Enter
47
+
48
+ >
40
49
 
41
50
  · npm install (el motor viene de la dependencia)
42
51
  ✓ claude: adaptador operativo (0 advertencia(s))
43
52
  ✓ planning válido: 0 épica(s), 0 tarea(s) en cola, 0 terminada(s)
44
- listo: el ciclo empieza en ops/planning/FLOW.md
53
+
54
+ 2 servicio(s) en /home/vos/mi-repo: apps/api, apps/web
55
+
56
+ ¿De qué trata este proyecto? Una línea alcanza.
57
+
58
+ Según lo que contestes salen hasta 3 preguntas más, con las palabras de
59
+ este proyecto, hasta cubrir lo que haga falta de esto:
60
+
61
+ · a quién sirve y quién lo usa
62
+ · cómo se sostiene: venta, suscripción, donación, presupuesto interno o trabajo voluntario
63
+ · qué querés que pase en este período y cómo se va a notar
64
+ · qué servicios o carpetas están muertos o fuera de alcance
65
+ · qué sistema externo o MCP hace falta conectar, y contra qué entorno
66
+
67
+ Mientras tanto, esto es lo que hay: apps/api, apps/web
68
+
69
+ → Abrí claude acá y contestale esa pregunta.
70
+ Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.
71
+ El ciclo empieza en ops/planning/FLOW.md.
45
72
  ```
46
73
 
47
74
  El default de las dos preguntas es no hacer nada: instalar un runner escribe en tu repositorio y
@@ -101,11 +128,13 @@ vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es l
101
128
  no puede hacer, así que ese recorrido vive en el runner:
102
129
 
103
130
  ```text
104
- /onboard parte del inventario de `ops scan` y escribe organization/, el «Mapa real» de
105
- AGENTS.md —cada comando como está declarado y de qué archivo salió— y las raíces
106
- de ops.config.json. Lo deducido queda marcado como supuesto; credenciales, MCP y
107
- el permiso de push van a HUMAN_ACTIONS.md. Cierra con la épica 001, sin promoverla.
108
- No corre nada del proyecto: verificar los comandos es una historia de esa épica.
131
+ /onboard te pregunta de qué trata el proyecto y, según lo que contestes, hasta tres más
132
+ con las palabras de ese proyecto —no un formulario que da por sentado que vendés
133
+ algo—. Con tus respuestas escribe organization/, el «Mapa real» de AGENTS.md y
134
+ las raíces de ops.config.json.
135
+ Lo deducido queda marcado como supuesto; credenciales, MCP y el permiso de push van
136
+ a HUMAN_ACTIONS.md. Cierra con la épica 001, sin promoverla. No corre nada del
137
+ proyecto: verificar los comandos es una historia de esa épica.
109
138
  /team evalúa si una intención posterior es viable y propone su épica.
110
139
  /autobuild ejecuta una tarea ya promovida, fase por fase.
111
140
  ```
@@ -143,6 +172,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
143
172
  |---|---|
144
173
  | `ops init [destino]` | Materializa una instancia y la deja usable; sin destino, en `ops/` y modo sidecar. |
145
174
  | `ops scan [workspace]` | Inventaría servicios y comandos declarados, sin correr ninguno. |
175
+ | `ops onboard [ops-root]` | Dice qué falta para arrancar y con qué pregunta empezar. |
146
176
  | `ops check <planning>` | Valida contratos, unicidad, trazabilidad y estados. |
147
177
  | `ops tree <planning>` | Muestra roadmap, backlog, WIP, inbox y done sin mutar nada. |
148
178
  | `ops context <planning>` | Emite el contexto mínimo de la tarea vigente para un runner. |
@@ -170,6 +200,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
170
200
  | `ops automation list-hooks <ops-root>` | Describe los guards portables disponibles. |
171
201
  | `ops automation check <ops-root>` | Valida guards, permisos y configuraciones. |
172
202
  | `ops automation install <ops-root> <runner>` | Instala el wiring de Claude, Codex, Antigravity o Gemini. |
203
+ | `ops automation uninstall <ops-root> <runner>` | Quita ese wiring y conserva lo que no escribió Cauce. |
173
204
  | `ops automation doctor <ops-root> <runner>` | Diagnostica una instalación materializada. |
174
205
 
175
206
  `ops --help` lista las banderas de cada uno. En un proyecto generado, `make help` muestra los atajos
@@ -210,7 +241,20 @@ y actualizar no exige resolver conflictos: se reemplaza `system/` entero y nada
210
241
  y sobrevive, desactivar uno del toolkit es quitarlo de la configuración del runner, y editar uno
211
242
  existente detiene el `upgrade` antes de pisarlo.
212
243
 
213
- ### Versionado
244
+ ### Actualizar
245
+
246
+ Son tres pasos y `make upgrade` hace los dos primeros:
247
+
248
+ ```bash
249
+ npm install --save-dev @ingeniomaps/cauce@latest # trae el motor nuevo
250
+ node tools/ops.js upgrade . # aplica system/ y el runtime
251
+ node tools/ops.js automation install . claude # el wiring del runner
252
+ ```
253
+
254
+ El primero no se puede saltear: `init` fija la versión exacta, así que `npm update` no la mueve y
255
+ `upgrade` compara contra el motor instalado —lo dice en su salida—. El tercero tampoco: los workflows y
256
+ las skills viven en el runner, no en la instancia, y `upgrade` no los toca. `upgrade` lo recuerda al
257
+ terminar.
214
258
 
215
259
  Como `upgrade` reemplaza `system/` sin pedir confirmación, un cambio en el protocolo, en una regla del
216
260
  sistema o en un guard es visible para el usuario y sube minor aunque no toque código. `upgrade` y
@@ -257,6 +301,12 @@ La instalación fusiona la configuración propia del runner y conserva las entra
257
301
  reemplaza los guards que el propio toolkit había registrado sueltos por el grupo que ahora los cubre, y
258
302
  lista cuáles quitó. Nada que no haya escrito el toolkit se toca.
259
303
 
304
+ Para sacarlo, `automation uninstall` quita exactamente lo que Cauce entregó y sigue igual que como lo
305
+ entregó: los guards de la configuración del runner, los workflows, los punteros a cargos. Lo que no
306
+ escribió —tus hooks, tus workflows, tus skills— queda donde está, y un archivo suyo que hayas editado se
307
+ conserva y se nombra en la salida. Borrar la carpeta ops sin esto deja al runner ejecutando guards que ya
308
+ no existen.
309
+
260
310
  Qué comprueba cada guard, qué no puede comprobar y cómo se agrupan por evento está en
261
311
  [automatization/hooks/README.md](automatization/hooks/README.md).
262
312
 
@@ -4,9 +4,19 @@ description: Escanea el repositorio y deja escrito el contexto de la empresa y l
4
4
  ---
5
5
 
6
6
  Es el arranque de una instancia recién creada: `init` la instaló, pero nadie le explicó todavía qué es
7
- este proyecto. Antes de empezar comprobá que siga vacía —`{{OPS_DIR}}organization/company.md` con sus
8
- «Por completar» y `{{OPS_DIR}}planning/roadmap/` sin épicas—: reescribir un contexto que alguien ya
9
- corrigió no deja rastro de lo que se perdió.
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.
10
20
 
11
21
  El inventario no lo hagas a mano: `node {{OPS_DIR}}tools/ops.js scan --json` devuelve los subproyectos con
12
22
  manifiesto propio, su runtime y los comandos que cada uno declara, con el archivo del que salieron.
@@ -24,6 +34,10 @@ Credenciales, MCP y el permiso de push no te corresponden. Cada uno va como fila
24
34
  `{{OPS_DIR}}planning/HUMAN_ACTIONS.md` con la acción concreta que lo desbloquea y sin proponer ningún
25
35
  valor; las preguntas abiertas, a la sección Ideas de `{{OPS_DIR}}planning/INBOX.md`.
26
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
+
27
41
  Cerrá escribiendo `epic-001` en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
28
42
  atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
29
43
  comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—. Validá
@@ -7,8 +7,16 @@
7
7
  @{{OPS_DIR}}planning/rules/system/commits.md
8
8
  @{{OPS_DIR}}planning/rules/system/conduct.md
9
9
 
10
- Los hooks de `.claude/settings.json` son obligatorios. En una instancia recién creada, `/onboard` escanea el
11
- repositorio y deja escrito el contexto de la empresa y la primera épica. Después, `/team` evalúa si una
10
+ Los hooks de `.claude/settings.json` son obligatorios. En una instancia recién creada, empezá corriendo
11
+ `node {{OPS_DIR}}tools/ops.js onboard`: es instantáneo, y lo primero que imprime es la pregunta con la que
12
+ tenés que abrir —de qué trata el proyecto—. Hacésela tal cual, antes de mirar el inventario y sea el
13
+ workspace vacío, un monorepo o diez repos. Recién con esa respuesta formulá hasta tres más, con las
14
+ palabras de ese proyecto y una por vez, hasta cubrir las dimensiones que haya listado. El inventario ya
15
+ viene resuelto en esa salida: no recorras el árbol ni leas código para completarlo. No des por sentado que vende
16
+ algo: puede sostenerse con donaciones, presupuesto interno o trabajo voluntario. Con las respuestas invocá
17
+ `/onboard <lo que te contó>`, que escribe el contexto y la primera épica; invocarlo antes sólo devuelve la
18
+ misma pregunta más caro. El arranque busca entender qué es el proyecto y dejar la instancia correcta para
19
+ él, no auditarlo: el análisis profundo viene después, cuando se pida algo concreto. Después, `/team` evalúa si una
12
20
  intención es viable y propone una épica, y `/autobuild` ejecuta trabajo ya promovido; `/integration-sync` e
13
21
  `/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
14
22
 
@@ -37,14 +37,22 @@ En una instancia recién creada nadie le explicó todavía al toolkit qué es es
37
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
38
  que alguien ya corrigió no deja rastro de lo que se perdió.
39
39
 
40
- Empezá por `node {{OPS_DIR}}tools/ops.js scan --json`, que devuelve los subproyectos con manifiesto propio
41
- y los comandos que cada uno declara: recorrer el árbol vos mismo cuesta minutos y encuentra lo mismo. Con
42
- eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con cada comando
43
- tal como está declarado y de qué archivo salió —sin correrlo—, y las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
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
44
48
  filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
45
49
  `{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
46
50
  lo que falta. Nunca la promuevas.
47
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
+
48
56
  ## Los equipos
49
57
 
50
58
  Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
@@ -30,14 +30,22 @@ En una instancia recién creada nadie le explicó todavía al toolkit qué es es
30
30
  `{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
31
31
  que alguien ya corrigió no deja rastro de lo que se perdió.
32
32
 
33
- Empezá por `node {{OPS_DIR}}tools/ops.js scan --json`, que devuelve los subproyectos con manifiesto propio
34
- y los comandos que cada uno declara: recorrer el árbol vos mismo cuesta minutos y encuentra lo mismo. Con
35
- eso escribí `{{OPS_DIR}}organization/`, la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con cada comando
36
- tal como está declarado y de qué archivo salió —sin correrlo—, y las raíces reales en `workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
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—, y después vienen el inventario y las
35
+ dimensiones. Hacé esa pregunta tal cual antes de mirar nada, sea el workspace vacío, un monorepo o diez
36
+ repos, y según lo que conteste formulá hasta tres más con las palabras de ese proyecto, una por vez. El
37
+ inventario ya viene resuelto ahí: no recorras el árbol ni leas código para completarlo. No des por
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
37
41
  filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
38
42
  `{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
39
43
  lo que falta. Nunca la promuevas.
40
44
 
45
+ El arranque tiene tres objetivos y ninguno más: entender qué es el proyecto, dejar la instancia correcta
46
+ para él y que la primera tarea pueda empezar. El análisis profundo viene después, cuando la persona pida
47
+ algo concreto.
48
+
41
49
  Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
42
50
  trabajo desde INBOX.
43
51
 
@@ -4,8 +4,13 @@
4
4
  //
5
5
  // El orden importa y se pagó caro: la primera versión le pedía a un agente que «inventariara el
6
6
  // repositorio», y en una carpeta vacía eso gastó doce minutos para no encontrar nada. Recorrer el árbol
7
- // es determinista y lo hace `ops scan` en milisegundos; el modelo entra después, y sólo si hay algo
8
- // sobre lo que decidir. Cuando no lo hay, este recorrido termina en una llamada.
7
+ // es determinista y lo hace `ops onboard` en milisegundos; el modelo entra después, y sólo si hay algo
8
+ // sobre lo que decidir.
9
+ //
10
+ // De ahí el techo: una llamada cuando falta contexto y tres cuando hay con qué escribir. No son fases
11
+ // separadas por prolijidad —escribir el contexto y registrar lo que le toca a una persona salen de la
12
+ // misma evidencia, así que salen juntas—, y ninguna sale a explorar: un arranque que hace esperar diez
13
+ // minutos ya no es un arranque.
9
14
  //
10
15
  // Escribe borradores y no decide por nadie: lo deducido queda marcado como supuesto, las credenciales y
11
16
  // los sistemas externos van a HUMAN_ACTIONS —R12 se los prohíbe a un runner— y la épica queda sin
@@ -15,11 +20,9 @@ export const meta = {
15
20
  description: 'Inventaría el workspace y deja escrito el contexto de la empresa y la primera épica',
16
21
  whenToUse: 'Primera corrida después de "cauce init", cuando organization/ y el roadmap están vacíos.',
17
22
  phases: [
18
- { title: 'Scan', detail: 'Inventario determinista: ops scan, sin modelo recorriendo nada' },
19
- { title: 'Draft', detail: 'organization/, mapa real y raíces de código' },
20
- { title: 'Human', detail: 'Credenciales, MCP y permisos: lo que no le toca al runner' },
21
- { title: 'Epic', detail: 'La épica que deja al ciclo poder correr' },
22
- { title: 'Closing', detail: 'check y lo que queda esperando a una persona' },
23
+ { title: 'Scan', detail: 'Estado e inventario, resueltos por el CLI y no por un modelo' },
24
+ { title: 'Draft', detail: 'organization/, mapa real, raíces y acciones humanas, de una pasada' },
25
+ { title: 'Epic', detail: 'La épica que deja al ciclo poder correr, y su check' },
23
26
  ],
24
27
  }
25
28
 
@@ -42,7 +45,15 @@ const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsabl
42
45
  `en un archivo de lo que estás suponiendo: lo segundo va marcado "(supuesto)" en el texto que escribas. ` +
43
46
  `No leas archivos de credenciales —.env, *.pem, claves— ni copies su contenido a ningún lado; ` +
44
47
  `.env.example sí, y sólo los nombres de las variables. No corras comandos del proyecto: este recorrido ` +
45
- `no ejecuta nada, sólo lee. No escribas en ningún sistema externo y no promuevas trabajo al BACKLOG.`
48
+ `no ejecuta nada. No escribas en ningún sistema externo y no promuevas trabajo al BACKLOG.\n\n` +
49
+ `Trabajá con lo que ya tenés: el inventario que devolvió el comando y lo que contestó la persona. No ` +
50
+ `recorras directorios, no leas código fuente y no abras más archivos que los que vas a escribir. Esto ` +
51
+ `es un arranque de cinco minutos, no una auditoría: lo que no esté a la vista se marca como supuesto o ` +
52
+ `queda como pregunta abierta, que es más barato y más honesto que averiguarlo.\n\n` +
53
+ `El arranque tiene tres objetivos y ninguno más: entender qué es este proyecto, dejar la instancia ` +
54
+ `correcta para él —contexto, mapa, raíces, lo que espera a una persona— y que la primera tarea pueda ` +
55
+ `empezar. El análisis profundo llega después, cuando alguien pida algo concreto; adelantarlo acá ` +
56
+ `retrasa el único momento en que la herramienta todavía no sirve para nada.`
46
57
 
47
58
  function finish(result) {
48
59
  log(`Fin: ${JSON.stringify(result)}`)
@@ -59,6 +70,11 @@ const SCAN = {
59
70
  properties: {
60
71
  fresh: { type: 'boolean' },
61
72
  reason: { type: 'string' },
73
+ // La conversación la enmarca el motor, no este recorrido: `ops onboard` da con qué pregunta empezar
74
+ // y qué dimensiones hay que cubrir, y duplicarlas acá dejaría dos listas que envejecen por separado.
75
+ opening: { type: 'string' },
76
+ followUps: { type: 'integer' },
77
+ dimensions: { type: 'array', items: { type: 'string' } },
62
78
  services: { type: 'array', items: { type: 'object', additionalProperties: false,
63
79
  required: ['path'], properties: {
64
80
  path: { type: 'string' },
@@ -91,16 +107,14 @@ phase('Scan')
91
107
  // de lo que devuelva, así que gastar más antes de saberlo es gastar a ciegas.
92
108
  const state = await agent(
93
109
  `${BASE}\n\nFrom ${ROOT}, run exactly these two commands and report what they printed. Explore nothing ` +
94
- `else and open no other file than the two named below.\n` +
95
- `1. "node tools/ops.js scan --json": the workspace inventory. Copy each service with its path, its ` +
96
- `runtimes and its declared commands, keeping the source file each command came from. Add nothing that ` +
97
- `the command did not print.\n` +
110
+ `else and open no file other than .env.example at the workspace root.\n` +
111
+ `1. "node tools/ops.js onboard --json": the instance state, the workspace inventory, the opening ` +
112
+ `question and the dimensions still uncovered. Copy fresh, opening, followUps, the "need" of each ` +
113
+ `dimension, and every service with its path, its runtimes and its declared commands keeping the source ` +
114
+ `file each command came from. Add nothing it did not print.\n` +
98
115
  `2. "node tools/ops.js check planning".\n` +
99
- `Then read ${ORG}/company.md and list ${ROADMAP}. Set fresh=true only if the organization file still ` +
100
- `carries the mold's "Por completar" placeholders and the roadmap holds nothing but its template and ` +
101
- `README; otherwise fresh=false and say in reason what is already written. If .env.example exists at ` +
102
- `the workspace root, report the variable names in secrets —names only— and the external services they ` +
103
- `point at in externals.`,
116
+ `If .env.example exists at the workspace root, report the variable names in secrets —names only— and ` +
117
+ `the external services they point at in externals.`,
104
118
  { schema: SCAN, label: 'inventario' },
105
119
  )
106
120
  if (!state) return stop('scan-unavailable', 'no se pudo leer el estado del workspace')
@@ -113,12 +127,21 @@ const services = state.services || []
113
127
  const listado = services.map((service) => service.path).join(', ')
114
128
  log(`${services.length} servicio(s) en el workspace${listado ? `: ${listado}` : ''}`)
115
129
 
116
- // Sin código y sin contexto no hay nada que escribir que no sea inventado, y esa es exactamente la
117
- // corrida que no puede costar nada: se termina acá, diciendo qué falta y cómo darlo.
118
- if (!services.length && !CONTEXT) {
119
- return stop('sin-contexto', `no hay servicios en el workspace y no me diste contexto, así que no hay ` +
120
- `nada que escribir sin inventarlo. Volvé a correrlo cuando estén los repos, o dame el contexto en ` +
121
- `una línea: /onboard qué vende la empresa, a quién, y cuál es el objetivo del trimestre.`)
130
+ // Sin contexto no hay nada que escribir que no sea inventado. Lo que se devuelve no es una negativa
131
+ // sino la conversación: quien recién instaló no sabe qué es «volvé a correrlo con contexto», y adivinar
132
+ // qué se espera de él es exactamente el trabajo que esta herramienta viene a sacarle de encima.
133
+ //
134
+ // Y se devuelve una pregunta con sus dimensiones, no un cuestionario: preguntarle «qué vende» a un
135
+ // proyecto libre, interno o sin fines de lucro es empezar por una respuesta que nadie dio.
136
+ const dimensions = state.dimensions || []
137
+ if (!CONTEXT && state.opening) {
138
+ log(`Falta lo que el repositorio no puede decir. Preguntale primero, con estas palabras:`)
139
+ log(` ${state.opening}`)
140
+ const faltan = dimensions.map((need) => ` · ${need}`).join('\n')
141
+ log(`Después, según lo que conteste, hasta ${state.followUps || 3} preguntas más, formuladas para este ` +
142
+ `proyecto y no como formulario, hasta cubrir lo que haga falta de:\n${faltan}`)
143
+ log('Con sus respuestas, volvé a invocar el arranque pasándoselas como contexto.')
144
+ return finish({ needsContext: true, opening: state.opening, dimensions, services: services.length })
122
145
  }
123
146
 
124
147
  const INVENTARIO = { services, externals: state.externals || [], secrets: state.secrets || [] }
@@ -129,10 +152,12 @@ const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTARIO)}` +
129
152
  phase('Draft')
130
153
 
131
154
  const drafted = await agent(
132
- `${BASE}\n\n${EVIDENCE}\n\nEscribí los borradores de contexto de esta instancia, reemplazando el molde ` +
133
- `en vez de comentarlo:\n` +
134
- `1. ${ORG}/company.md y ${ORG}/product.md: lo que el contexto aportado y los nombres del repositorio ` +
135
- `permiten afirmar. Lo que nada sostiene queda "Por definir" y su pregunta va a openQuestions.\n` +
155
+ `${BASE}\n\n${EVIDENCE}\n\nEscribí de una sola pasada el contexto de esta instancia, reemplazando el ` +
156
+ `molde en vez de comentarlo:\n` +
157
+ `1. ${ORG}/company.md y ${ORG}/product.md: lo que la persona contó y los nombres del repositorio ` +
158
+ `permiten afirmar. Lo que nada sostiene queda "Por definir" y su pregunta va a openQuestions. No des ` +
159
+ `por sentado que el proyecto vende algo: puede sostenerse con donaciones, presupuesto interno o ` +
160
+ `trabajo voluntario, y una sección que no aplica se dice, no se completa con algo plausible.\n` +
136
161
  `2. La sección "## Mapa real" de ${ROOT}/AGENTS.md: una entrada por servicio con su ruta, su runtime y ` +
137
162
  `sus comandos **tal como los declara**, diciendo de qué archivo salió cada uno. No afirmes que ` +
138
163
  `funcionan: nadie los corrió. Un servicio sin comandos declarados se escribe así, que es información.\n` +
@@ -140,26 +165,16 @@ const drafted = await agent(
140
165
  `usa para bloquear una escritura fuera de lugar. Una raíz de más lo apaga.\n` +
141
166
  `${services.length ? '' : 'Sin servicios, el mapa queda declarado como pendiente, diciendo qué lo ' +
142
167
  'completa.\n'}` +
168
+ `4. ${HUMAN}: una fila por cada cosa que necesita a una persona, con la tarea, el estado pendiente, el ` +
169
+ `origen "onboard" y la acción concreta que la desbloquea. Como mínimo, una por cada credencial que el ` +
170
+ `proyecto espera —dónde se cargan y quién lo hace, sin proponer ningún valor—, una por cada sistema ` +
171
+ `externo o MCP a conectar, y una por la autoridad del runner, que hoy declara runner.allowPush=false.\n` +
172
+ `5. Las preguntas que queden abiertas, en la sección Ideas de ${INBOX}, sin promover.\n` +
143
173
  `Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
144
174
  { schema: WRITTEN, label: 'contexto' },
145
175
  )
146
176
  if (!drafted) return stop('draft-unavailable', 'los borradores no devolvieron resultado')
147
177
 
148
- phase('Human')
149
-
150
- // Credenciales, MCP y permiso de push no son trabajo del runner: R12 se los prohíbe y R13 exige dejar
151
- // dicho quién los resuelve y con qué. La fila vale más que la negativa.
152
- const pending = await agent(
153
- `${BASE}\n\n${EVIDENCE}\n\nRegistrá en ${HUMAN} una fila por cada cosa que necesita a una persona, con ` +
154
- `la tarea, el estado pendiente, el origen "onboard" y la acción concreta que la desbloquea. Como ` +
155
- `mínimo: una por cada credencial que el proyecto espera —dónde se cargan y quién lo hace, sin proponer ` +
156
- `ningún valor—, una por cada servicio externo o MCP a conectar —con su alcance y contra qué entorno— y ` +
157
- `una por la autoridad del runner, que hoy declara runner.allowPush=false en ops.config.json. Las ` +
158
- `preguntas abiertas van a la sección Ideas de ${INBOX}, sin promover: ` +
159
- `${JSON.stringify(drafted.openQuestions || [])}`,
160
- { schema: WRITTEN, label: 'acciones-humanas' },
161
- )
162
-
163
178
  phase('Epic')
164
179
 
165
180
  const epic = await agent(
@@ -176,29 +191,22 @@ const epic = await agent(
176
191
  `${services.length
177
192
  ? 'Verificar los comandos es una historia: nadie los corrió todavía.'
178
193
  : 'La primera historia es traer los repos y declararlos en workspaceRoots.'} ` +
179
- `En "## Riesgos y decisiones humanas" citá las filas que quedaron en HUMAN_ACTIONS. No toques BACKLOG.md.`,
180
- { schema: { type: 'object', additionalProperties: false, required: ['file'],
194
+ `En "## Riesgos y decisiones humanas" citá las filas que quedaron en HUMAN_ACTIONS. No toques ` +
195
+ `BACKLOG.md.\n\n` +
196
+ `Cerrá corriendo "node tools/ops.js check planning" desde ${ROOT} y, si falla, reparando sólo lo que ` +
197
+ `esta corrida escribió; nunca debilites un criterio para forzar el verde.`,
198
+ { schema: { type: 'object', additionalProperties: false, required: ['file', 'passed'],
181
199
  properties: {
182
- file: { type: 'string' }, title: { type: 'string' },
200
+ file: { type: 'string' }, passed: { type: 'boolean' }, details: { type: 'string' },
183
201
  criteria: { type: 'array', items: { type: 'string' } },
184
202
  stories: { type: 'array', items: { type: 'string' } },
185
203
  } }, label: 'epica-001' },
186
204
  )
187
205
  if (!epic) return stop('epic-unavailable', 'la épica no devolvió resultado')
188
-
189
- phase('Closing')
190
-
191
- const closing = await agent(
192
- `${BASE}\n\nFrom ${ROOT}, run "node tools/ops.js check planning" and report whether it passed. If it ` +
193
- `failed, repair only what this run wrote —the epic, the config— so it satisfies the contract; never ` +
194
- `weaken a criterion to force green.`,
195
- { schema: { type: 'object', additionalProperties: false, required: ['passed', 'details'],
196
- properties: { passed: { type: 'boolean' }, details: { type: 'string' } } }, label: 'closing-check' },
197
- )
198
- if (!closing || !closing.passed) return stop('check-failed', closing ? closing.details : 'sin resultado')
206
+ if (!epic.passed) return stop('check-failed', epic.details || 'check no pasó tras escribir la épica')
199
207
 
200
208
  const supuestos = (drafted.assumptions || []).length
201
- const acciones = ((pending || {}).humanActions || []).length
209
+ const acciones = (drafted.humanActions || []).length
202
210
  log(`Contexto escrito con ${supuestos} supuesto(s) por confirmar y ${acciones} acción(es) humana(s) en ${HUMAN}.`)
203
211
  log(`Épica en ${epic.file}, sin promover: revisala, promoví una historia a un hito del BACKLOG y corré /autobuild.`)
204
212
 
@@ -478,6 +478,108 @@ function deliveryState(recorded, name, resolved, prefix = '') {
478
478
  return delivered && delivered === current ? 'desactualizado' : 'ajeno'
479
479
  }
480
480
 
481
+ // Quita de una estructura de configuración exactamente lo que este adaptador habría puesto, y nada más.
482
+ // Es el inverso de `mergeConfig`: una entrada del usuario nunca coincide literalmente con la nuestra, así
483
+ // que sobrevive; una que editó tampoco coincide, y por eso se conserva y se avisa en vez de borrarse.
484
+ function unmergeConfig(current, incoming) {
485
+ if (Array.isArray(incoming)) {
486
+ if (!Array.isArray(current)) return current
487
+ const nuestras = new Set(incoming.map((value) => JSON.stringify(value)))
488
+ return current.filter((value) => !nuestras.has(JSON.stringify(value)))
489
+ }
490
+ if (incoming && typeof incoming === 'object') {
491
+ if (!current || typeof current !== 'object' || Array.isArray(current)) return current
492
+ const result = { ...current }
493
+ for (const [key, value] of Object.entries(incoming)) {
494
+ if (!(key in result)) continue
495
+ const limpio = unmergeConfig(result[key], value)
496
+ // Una clave que queda vacía por habernos ido no es del usuario: la creamos nosotros al instalar.
497
+ const vacia = limpio === undefined
498
+ || (Array.isArray(limpio) && !limpio.length)
499
+ || (limpio && typeof limpio === 'object' && !Array.isArray(limpio) && !Object.keys(limpio).length)
500
+ if (vacia) delete result[key]
501
+ else result[key] = limpio
502
+ }
503
+ return result
504
+ }
505
+ return JSON.stringify(current) === JSON.stringify(incoming) ? undefined : current
506
+ }
507
+
508
+ // Borra el archivo y, de paso, los directorios que quedaron vacíos por haberlo sacado. Nunca sube más
509
+ // allá del límite: `.claude/` puede tener cosas del usuario aunque `.claude/workflows/` quede vacío.
510
+ function removeFile(file, boundary) {
511
+ fs.rmSync(file, { force: true })
512
+ let dir = path.dirname(file)
513
+ while (dir.startsWith(boundary) && dir !== boundary) {
514
+ try { if (fs.readdirSync(dir).length) return } catch { return }
515
+ fs.rmdirSync(dir)
516
+ dir = path.dirname(dir)
517
+ }
518
+ }
519
+
520
+ // Saca el wiring de un runner dejando intacto lo que no escribimos nosotros.
521
+ //
522
+ // Existe porque desinstalar a mano es borrar `ops/` y descubrir después que cada llamada de herramienta
523
+ // ejecuta un guard que ya no está. Y porque la alternativa —borrar `.claude/` entero— se lleva puesto lo
524
+ // que el usuario haya puesto ahí, que es suyo y no tiene por qué desaparecer con el toolkit.
525
+ //
526
+ // La regla es una sola: se quita lo que Cauce entregó y sigue igual que como lo entregó. Un archivo con
527
+ // cambios propios se conserva y se nombra; decidir sobre él es de la persona, no de este comando.
528
+ function uninstall(root, name, output = console) {
529
+ if (O.mode(root) === 'toolkit') {
530
+ throw new Error('Acá se fabrica Cauce, no se lo consume.')
531
+ }
532
+ const runner = runnerManifest(root, name)
533
+ const paths = runnerPaths(root, name, runner)
534
+ const prefix = opsPrefix(root)
535
+ const recorded = M.readRunners(root)
536
+ const conservados = []
537
+ let quitados = 0
538
+
539
+ const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
540
+ const entregado = { ...recorded }
541
+ for (const item of items) {
542
+ const resolved = { item, ...resolveItem(paths, root, name, item) }
543
+ const key = deliveryKey(name, item.target)
544
+ if (!fs.existsSync(resolved.target)) { delete entregado[key]; continue }
545
+ const situacion = deliveryState(recorded, name, resolved, prefix)
546
+ if (situacion === 'ajeno') { conservados.push(item.target); continue }
547
+ removeFile(resolved.target, paths.install)
548
+ delete entregado[key]
549
+ quitados += 1
550
+ }
551
+
552
+ // Los punteros a cargos no se registran uno por uno —son cuarenta y siete y se regeneran enteros—,
553
+ // así que se reconocen por contenido: sólo se va el que sigue siendo el que generamos.
554
+ if (runner.capabilities.nativeSkills && runner.roleSkills) {
555
+ const base = path.resolve(paths.install, runner.roleSkills)
556
+ for (const role of roleCatalog(root)) {
557
+ const file = path.join(base, role.slug, 'SKILL.md')
558
+ if (!fs.existsSync(file)) continue
559
+ if (M.digest(file) !== M.digestText(roleSkill(role))) {
560
+ conservados.push(path.relative(paths.install, file))
561
+ continue
562
+ }
563
+ removeFile(file, paths.install)
564
+ quitados += 1
565
+ }
566
+ }
567
+
568
+ if (fs.existsSync(paths.configTarget)) {
569
+ const current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
570
+ const limpio = unmergeConfig(current, runnerConfig(paths, root))
571
+ if (limpio && Object.keys(limpio).length) F.atomicWriteJson(paths.configTarget, limpio)
572
+ else { removeFile(paths.configTarget, paths.install); quitados += 1 }
573
+ output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
574
+ }
575
+
576
+ M.write(root, undefined, entregado)
577
+ output.log(`✓ ${name}: ${quitados} archivo(s) del toolkit quitados de ${paths.install}`)
578
+ for (const file of conservados) output.log(`= ${name}: conservado ${file} (tiene cambios tuyos)`)
579
+ if (runner.activation) output.log(` ${name}: si lo habías registrado a mano, quitalo también.`)
580
+ return { removed: quitados, kept: conservados }
581
+ }
582
+
481
583
  function install(root, name, output = console, options = {}) {
482
584
  // `install` arma la superficie de consumo de una empresa: punteros a cada cargo, una copia de los
483
585
  // workflows y los guards. Acá los cargos y los workflows son el producto —la copia divergiría— y
@@ -571,6 +673,7 @@ module.exports = {
571
673
  check,
572
674
  doctor,
573
675
  install,
676
+ uninstall,
574
677
  legacyGuardWiring,
575
678
  roleCatalog,
576
679
  roleSkill,
@@ -15,6 +15,7 @@ const VALUED_FLAGS = new Set([
15
15
  const FLAGS = {
16
16
  init: ['--name', '--mode', '--force', '--runner', '--integration', '--install', '--no-install'],
17
17
  scan: ['--json'],
18
+ onboard: ['--json'],
18
19
  check: ['--json'],
19
20
  tree: ['--json', '--no-color'],
20
21
  context: ['--json'],
@@ -25,10 +25,14 @@ function terminal() {
25
25
  // dejar que ese rechazo suba terminaba la corrida con «Aborted with Ctrl+D» y la instancia recién creada
26
26
  // sin una línea que dijera cómo seguir. El default no hace nada, así que tomarlo no decide nada.
27
27
  async function elegir(deps, texto, opciones, fallback) {
28
- const listado = opciones.map((opcion, indice) => `${indice + 1}) ${opcion}`).join(' ')
28
+ // Una opción por línea y el default señalado donde se mira: apretar Enter es la respuesta más común, y
29
+ // en una sola línea apretada el `[ninguno]` del final no se lee como «esto pasa si no elegís nada».
30
+ const listado = opciones
31
+ .map((opcion, indice) => ` ${indice + 1}) ${opcion}${opcion === fallback ? ' ← Enter' : ''}`)
32
+ .join('\n')
29
33
  for (let intento = 0; intento < INTENTOS; intento += 1) {
30
34
  let dicho
31
- try { dicho = await deps.ask(`\n${texto}\n${listado}\n[${fallback}] > `) } catch {
35
+ try { dicho = await deps.ask(`\n${texto}\n\n${listado}\n\n> `) } catch {
32
36
  deps.log(`\n sin respuesta: sigo con ${fallback}.`)
33
37
  return fallback
34
38
  }
package/engine/cli/ops.js CHANGED
@@ -15,6 +15,7 @@ const O = require('../core/ownership')
15
15
  const CL = require('../core/changelog')
16
16
  const M = require('../core/manifest')
17
17
  const SC = require('../core/scan')
18
+ const OB = require('../core/onboarding')
18
19
  const C = require('../config/validate')
19
20
  const T = require('../teams/registry')
20
21
  const AG = require('../agents/catalog')
@@ -27,6 +28,10 @@ const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
27
28
  // Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
28
29
  const DEFAULT_TARGET = 'ops'
29
30
 
31
+ // Cuántos servicios se listan en pantalla antes de recortar. El resto sigue en `--json`, que es lo que
32
+ // consume el recorrido de arranque: recortar la lista es para leerla, no para acotar lo que se sabe.
33
+ const LISTA = 20
34
+
30
35
  function fail(message, code = 1) {
31
36
  console.error(message)
32
37
  process.exit(code)
@@ -37,6 +42,7 @@ function usage() {
37
42
  ops init [destino] [--name <nombre>] [--mode embedded|sidecar] [--force]
38
43
  [--runner claude|codex|gemini|antigravity] [--integration <proveedor>] [--install|--no-install]
39
44
  ops scan [workspace] [--json]
45
+ ops onboard [ops-root] [--json]
40
46
  ops check <planning-dir> [--json]
41
47
  ops tree <planning-dir> [--no-color] [--json]
42
48
  ops context <planning-dir> [--json]
@@ -57,6 +63,7 @@ function usage() {
57
63
  ops automation check <ops-root>
58
64
  ops automation doctor <ops-root> claude|codex|gemini|antigravity
59
65
  ops automation install <ops-root> claude|codex|gemini|antigravity
66
+ ops automation uninstall <ops-root> claude|codex|gemini|antigravity
60
67
  ops learn <agent> [--proposal] [--applied [--period <AAAA-MM>]]
61
68
  ops evaluate <agent> [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
62
69
  ops agents list [ops-root] [--own|--system] [--json]
@@ -318,41 +325,63 @@ async function init(target, cli) {
318
325
  })
319
326
  } catch (error) { fail(error.message, 2) }
320
327
 
321
- if (resultado.instalado) {
322
- check(path.join(root, 'planning'), SIN_BANDERAS)
323
- // Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el
324
- // molde y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que
325
- // es justo lo que un CLI determinista no puede hacer; el recorrido vive en el runner, así que lo
326
- // único útil acá es decir cuál es y con qué se abre.
327
- if (resultado.runner !== BOOT.SIN_RUNNER) {
328
- console.log(` siguiente: abrí ${resultado.runner} en este directorio y corré /onboard`)
329
- console.log(' escanea el repositorio y deja escrito el contexto de la empresa y la primera épica')
330
- }
331
- console.log(` listo: el ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}`)
332
- }
328
+ if (resultado.instalado) check(path.join(root, 'planning'), SIN_BANDERAS)
329
+
330
+ // Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el molde
331
+ // y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es justo
332
+ // lo que un CLI determinista no puede hacer; lo que puede es decir qué falta y qué preguntar.
333
+ //
334
+ // Se imprime siempre, incluso cuando la dependencia no se instaló: lo resuelve el motor que está
335
+ // corriendo `init`, no cuesta nada, y es lo único que le dice a alguien qué hacer con lo que acaba de
336
+ // crear. Dejarlo adentro del camino feliz lo escondía justo de quien más lo necesita.
337
+ console.log('')
338
+ onboard(root, SIN_BANDERAS, resultado.instalado ? resultado.runner : '')
333
339
  for (const paso of initSteps(enter, resultado)) console.log(paso)
334
340
  if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
335
341
  }
336
342
 
337
- // Qué hay en el workspace, antes de que nadie razone sobre él. La raíz ops se saltea: no es un servicio
338
- // del proyecto, y su `package.json` sólo declara el motor.
343
+ // Dónde puede mirar una instancia: exactamente las raíces que declara, y nada por encima de ellas. Sale
344
+ // de `ops.config.json` en vez de suponerse —el sidecar declara `..`, el embebido `.`— para que acotar las
345
+ // raíces acote también el escaneo, y para que nadie termine recorriendo la carpeta de al lado.
346
+ function workspaceRoots(root) {
347
+ try {
348
+ const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
349
+ const declared = (config.workspaceRoots || []).map((entry) => path.resolve(root, entry.path || '.'))
350
+ return declared.length ? declared : [root]
351
+ } catch { return [root] }
352
+ }
353
+
354
+ // Qué hay en las raíces declaradas, antes de que nadie razone sobre ello. La raíz ops se saltea: no es
355
+ // un servicio del proyecto, y su `package.json` sólo declara el motor.
356
+ function inventory(root) {
357
+ const found = []
358
+ for (const workspace of workspaceRoots(root)) {
359
+ const result = SC.scan(workspace, root)
360
+ if (result.rootManifests.length) {
361
+ found.push({ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands })
362
+ }
363
+ for (const service of result.services) found.push({ ...service, root: workspace })
364
+ }
365
+ return found
366
+ }
367
+
339
368
  function scan(target, cli) {
340
- // Sólo el sidecar tiene su workspace afuera: la instancia es hija de la carpeta donde vive el código.
341
- // Cualquier otro modo —embedded, y este mismo repositorio, que es `toolkit`— escanea donde está
342
- // parado. Confundirlos hacía que un `scan` sin argumentos se fuera a recorrer la carpeta de al lado.
343
- const sidecar = O.mode(process.cwd()) === 'sidecar'
344
- const root = path.resolve(target || (sidecar ? path.join(process.cwd(), '..') : '.'))
345
- const result = SC.scan(root, sidecar ? process.cwd() : '')
369
+ const root = path.resolve(target || '.')
370
+ const result = target
371
+ ? { root: path.resolve(target), services: SC.scan(path.resolve(target)).services }
372
+ : { root, services: inventory(root) }
346
373
  if (cli.has('--json')) return console.log(JSON.stringify(result, null, 2))
347
- console.log(`workspace ${result.root}`)
348
- if (result.rootManifests.length) {
349
- console.log(`. ${result.rootManifests.join(', ')}${comandos(result.rootCommands)}`)
374
+ // Un monorepo de sesenta paquetes no se lee en pantalla. Se recorta, y se dice cuánto: un corte que no
375
+ // se anuncia hace pasar lo listado por todo lo que hay.
376
+ for (const service of result.services.slice(0, LISTA)) {
377
+ const donde = service.root && service.root !== result.root ? `${path.basename(service.root)}/` : ''
378
+ console.log(`${donde}${service.path} [${(service.runtimes || []).join(', ')}]${comandos(service.commands)}`)
350
379
  }
351
- for (const service of result.services) {
352
- console.log(`${service.path} [${service.runtimes.join(', ')}]${comandos(service.commands)}`)
380
+ if (result.services.length > LISTA) {
381
+ console.log(`… y ${result.services.length - LISTA} más, todos en --json`)
353
382
  }
354
- const total = result.services.length + (result.rootManifests.length ? 1 : 0)
355
- console.log(`${total} candidato(s). Cuál es el producto y cuál quedó muerto lo decide una persona.`)
383
+ console.log(`${result.services.length} candidato(s). Cuál es el producto y cuál quedó muerto lo ` +
384
+ 'decide una persona.')
356
385
  }
357
386
 
358
387
  // Sólo lo declarado y de dónde salió: un comando inventado se lee igual que uno real.
@@ -362,6 +391,43 @@ function comandos(commands) {
362
391
  return ` — ${entries.map(([kind, value]) => `${kind}: ${value.command} (${value.source})`).join(', ')}`
363
392
  }
364
393
 
394
+ // La guía de arranque: qué hay, qué falta y qué preguntar. Determinista y en milisegundos, porque es lo
395
+ // primero que ve alguien que acaba de instalar y todavía no sabe qué hace la herramienta.
396
+ function onboard(rootArg, cli, runner = '') {
397
+ const root = path.resolve(rootArg || '.')
398
+ const services = inventory(root)
399
+ const state = OB.guide(root, services)
400
+ if (cli.has('--json')) {
401
+ return console.log(JSON.stringify({ ...state, roots: workspaceRoots(root), servicios: services }, null, 2))
402
+ }
403
+ // La pregunta primero, y el inventario después: de qué trata el proyecto es lo mismo esté vacío,
404
+ // sea un monorepo o sean diez repos, y empezar por lo que se encontró invierte de qué se trata esto.
405
+ if (state.fresh) {
406
+ console.log(`${state.opening}\n`)
407
+ console.log(`Según lo que contestes salen hasta ${state.followUps} preguntas más, con las palabras de`)
408
+ console.log('este proyecto, hasta cubrir lo que haga falta de esto:\n')
409
+ for (const dimension of state.dimensions) console.log(` · ${dimension.need}`)
410
+ console.log('')
411
+ }
412
+ const nombres = services.slice(0, LISTA).map((service) => service.path).join(', ')
413
+ const resto = services.length > LISTA ? ` y ${services.length - LISTA} más` : ''
414
+ console.log(services.length
415
+ ? `Mientras tanto, esto es lo que hay: ${nombres}${resto}`
416
+ : 'Mientras tanto, en el workspace todavía no hay ningún proyecto.')
417
+ if (!state.fresh) {
418
+ const escrito = [state.written.organization && 'organization/', state.written.roadmap && 'el roadmap']
419
+ .filter(Boolean).join(' y ')
420
+ console.log(`Esta instancia ya tiene ${escrito} escrito: el arranque no la va a pisar.`)
421
+ return
422
+ }
423
+ // Un solo cierre: tres líneas que suenan a final se leen como tres finales, y quien recién instaló
424
+ // termina sin saber cuál era el paso.
425
+ console.log(runner
426
+ ? `\n→ Abrí ${runner} acá y contestale esa pregunta.`
427
+ : '\n→ Contestá esa pregunta cuando corras el arranque.')
428
+ console.log(' Con tus respuestas escribe organization/, el mapa real de AGENTS.md y la primera épica.')
429
+ }
430
+
365
431
  function check(dir, cli) {
366
432
  const root = path.resolve(dir || '.')
367
433
  const errors = []
@@ -675,7 +741,13 @@ function upgrade(dir, cli) {
675
741
  const overrides = O.overrides(root)
676
742
 
677
743
  if (dry) {
678
- if (from === to) return console.log(`= ${to}: la instancia está al día`)
744
+ if (from === to) {
745
+ // Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
746
+ // importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
747
+ // a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
748
+ console.log(`= ${to}: la instancia está al día con el motor instalado`)
749
+ return console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
750
+ }
679
751
  console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
680
752
  printChangelog(from, to)
681
753
  for (const file of changed) console.log(` editado localmente: ${file}`)
@@ -1044,6 +1116,11 @@ function automation(action, rootArg, runnerName, cli) {
1044
1116
  console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
1045
1117
  return
1046
1118
  }
1119
+ if (action === 'uninstall') {
1120
+ try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message, 2) }
1121
+ console.log(' la instancia sigue en pie: borrar la carpeta ops es una decisión aparte.')
1122
+ return
1123
+ }
1047
1124
  if (action === 'install') {
1048
1125
  let runner
1049
1126
  const force = cli.has('--force')
@@ -1169,6 +1246,7 @@ async function run(cli) {
1169
1246
  const arg = cli.positional
1170
1247
  if (command === 'init') await init(arg[1], cli)
1171
1248
  else if (command === 'scan') scan(arg[1], cli)
1249
+ else if (command === 'onboard') onboard(arg[1], cli)
1172
1250
  else if (command === 'check') check(arg[1], cli)
1173
1251
  else if (command === 'tree') tree(arg[1], cli)
1174
1252
  else if (command === 'context') context(arg[1], cli)
@@ -0,0 +1,67 @@
1
+ 'use strict'
2
+
3
+ // Qué le falta a una instancia para poder arrancar, y qué preguntarle a quien la creó. Vive en el motor
4
+ // —y no en el recorrido del runner— porque es determinista y tiene que costar cero: la versión anterior
5
+ // gastaba un subagente de un minuto para terminar diciendo «volvé a correrlo con contexto», que a quien
6
+ // no conoce la herramienta no le dice nada. Una pregunta escrita es la diferencia entre guiar y mandar
7
+ // a averiguar.
8
+
9
+ const fs = require('node:fs')
10
+ const path = require('node:path')
11
+
12
+ // El molde llega con estos marcadores. Que sigan ahí es la señal de que nadie escribió todavía.
13
+ const PLACEHOLDERS = /Por completar|Por definir/
14
+
15
+ // La única pregunta que no depende de ninguna respuesta, y por eso la única que se puede escribir de
16
+ // antemano. Las cuatro fijas que había antes daban por sentado que el proyecto vende algo: a uno libre,
17
+ // interno o sin fines de lucro le preguntaban quién paga antes de saber de qué se trataba.
18
+ const OPENING = '¿De qué trata este proyecto? Una línea alcanza.'
19
+
20
+ // Lo que hay que cubrir para poder escribir `organization/`, no cómo preguntarlo: la pregunta concreta
21
+ // la formula quien conduce la conversación, con las palabras de este proyecto, y en un proyecto libre
22
+ // «cómo se sostiene» se pregunta de una manera que en una empresa no tendría sentido. Son dimensiones,
23
+ // no un formulario, y quien pregunta puede cubrir dos con una sola pregunta si vienen juntas.
24
+ const DIMENSIONS = [
25
+ { key: 'quien', need: 'a quién sirve y quién lo usa' },
26
+ { key: 'sostiene',
27
+ need: 'cómo se sostiene: venta, suscripción, donación, presupuesto interno o trabajo voluntario' },
28
+ { key: 'exito', need: 'qué querés que pase en este período y cómo se va a notar' },
29
+ { key: 'alcance', need: 'qué servicios o carpetas están muertos o fuera de alcance' },
30
+ { key: 'externos', need: 'qué sistema externo o MCP hace falta conectar, y contra qué entorno' },
31
+ { key: 'codigo', need: 'dónde está el código, que todavía no aparece en el workspace' },
32
+ ]
33
+
34
+ // Tres seguidas ya son una conversación; más, un formulario. La apertura no cuenta: es la que decide
35
+ // cuáles de las demás valen la pena.
36
+ const FOLLOW_UPS = 3
37
+
38
+ function organizationWritten(root) {
39
+ const file = path.join(root, 'organization', 'company.md')
40
+ try { return !PLACEHOLDERS.test(fs.readFileSync(file, 'utf8')) } catch { return false }
41
+ }
42
+
43
+ function roadmapWritten(root) {
44
+ const dir = path.join(root, 'planning', 'roadmap')
45
+ try {
46
+ return fs.readdirSync(dir).some((name) => /^epic-\d+/.test(name) && !/^epic-000/.test(name))
47
+ } catch { return false }
48
+ }
49
+
50
+ // El estado de una instancia, la pregunta con la que se empieza y lo que queda por cubrir. `services`
51
+ // viene del escaneo: sin código, preguntar por el alcance no tiene sobre qué caer, y preguntar dónde
52
+ // está el código sí.
53
+ function guide(root, services = []) {
54
+ const written = { organization: organizationWritten(root), roadmap: roadmapWritten(root) }
55
+ const fresh = !written.organization && !written.roadmap
56
+ const irrelevant = services.length ? 'codigo' : 'alcance'
57
+ return {
58
+ fresh,
59
+ written,
60
+ services: services.length,
61
+ opening: fresh ? OPENING : '',
62
+ followUps: fresh ? FOLLOW_UPS : 0,
63
+ dimensions: fresh ? DIMENSIONS.filter((dimension) => dimension.key !== irrelevant) : [],
64
+ }
65
+ }
66
+
67
+ module.exports = { guide, OPENING, DIMENSIONS, FOLLOW_UPS, PLACEHOLDERS }
@@ -15,9 +15,31 @@ const path = require('node:path')
15
15
  // y un servicio del producto no se esconde detrás de un punto.
16
16
  const IGNORED = new Set([
17
17
  'node_modules', 'vendor', 'dist', 'build', 'target', 'out', 'coverage', 'venv', '__pycache__', 'tmp',
18
+ 'bower_components', 'jspm_packages', 'Pods', 'DerivedData', 'elm-stuff', '_build', 'deps', 'obj',
19
+ 'site-packages', 'dist-newstyle', 'htmlcov', 'storybook-static', 'logs',
18
20
  ])
19
21
 
20
- const skippable = (name) => name.startsWith('.') || IGNORED.has(name)
22
+ // Lo que este proyecto ya declaró que no es suyo. Leer el `.gitignore` de la raíz sale gratis y ahorra
23
+ // mantener una lista de basura ajena: cada proyecto tiene la suya, y el nuestro no la puede adivinar.
24
+ //
25
+ // Se toman sólo los patrones que nombran un directorio sin comodines —`build/`, `/dist`, `.cache`—, y se
26
+ // aplican por nombre en cualquier nivel, que es más ancho que la semántica real de git. Para decidir si
27
+ // vale la pena entrar a mirar un directorio alcanza; para cualquier otra cosa, no es un parser de
28
+ // gitignore y no hay que usarlo como si lo fuera.
29
+ function ignoredByGit(root) {
30
+ let text = ''
31
+ try { text = fs.readFileSync(path.join(root, '.gitignore'), 'utf8') } catch { return [] }
32
+ return text.split('\n')
33
+ .map((line) => line.trim())
34
+ .filter((line) => line && !line.startsWith('#') && !line.startsWith('!') && !/[*?[\]]/.test(line))
35
+ .map((line) => line.replace(/^\/+/, '').replace(/\/+$/, ''))
36
+ .filter((line) => line && !line.includes('/'))
37
+ }
38
+
39
+ const skipper = (root) => {
40
+ const declared = new Set(ignoredByGit(root))
41
+ return (name) => name.startsWith('.') || IGNORED.has(name) || declared.has(name)
42
+ }
21
43
 
22
44
  // Un servicio anidado más hondo que esto es una excepción, y recorrer el árbol entero para encontrarlo
23
45
  // cuesta más que declararlo a mano en `AGENTS.md`.
@@ -82,6 +104,7 @@ function manifestsOf(dir) {
82
104
  function services(root, skip = '') {
83
105
  const found = []
84
106
  const excluded = skip ? path.resolve(skip) : ''
107
+ const skippable = skipper(root)
85
108
  const walk = (dir, depth) => {
86
109
  const manifests = manifestsOf(dir)
87
110
  if (manifests.length && path.resolve(dir) !== path.resolve(root)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
package/template/Makefile CHANGED
@@ -4,6 +4,7 @@
4
4
  .PHONY: integration-check integration-sync require-key integration-promote
5
5
  .PHONY: require-agent agent-learn agent-propose agent-evaluate require-team team-check team-show
6
6
  .PHONY: install-claude install-codex install-gemini install-antigravity
7
+ .PHONY: uninstall-claude uninstall-codex uninstall-gemini uninstall-antigravity
7
8
  .PHONY: doctor-claude doctor-codex doctor-gemini doctor-antigravity
8
9
 
9
10
  # Jira es hoy el único proveedor; el día que haya otro se pasa PROVIDER=<slug>.
@@ -22,7 +23,10 @@ tree: ## Muestra roadmap, backlog, WIP y Done
22
23
  context: ## Muestra el contexto mínimo de la tarea vigente
23
24
  @node tools/ops.js context planning
24
25
 
25
- upgrade: ## Actualiza Cauce sin tocar lo del proyecto
26
+ # `init` fija la versión exacta del motor, así que `npm update` no la mueve: hay que pedir @latest.
27
+ # Sin este primer paso `upgrade` compara contra el motor instalado y contesta «al día» para siempre.
28
+ upgrade: ## Trae la última versión de Cauce y la aplica, sin tocar lo del proyecto
29
+ @npm install --save-dev @ingeniomaps/cauce@latest
26
30
  @node tools/ops.js upgrade .
27
31
 
28
32
  automation-check: ## Comprueba hooks, workflows y adaptadores
@@ -73,6 +77,18 @@ install-gemini: ## Instala contexto, comandos y configuración para Gemini
73
77
  install-antigravity: ## Instala el plugin Cauce para Antigravity CLI
74
78
  @node tools/ops.js automation install . antigravity
75
79
 
80
+ uninstall-claude: ## Quita de Claude el wiring de Cauce, conservando lo tuyo
81
+ @node tools/ops.js automation uninstall . claude
82
+
83
+ uninstall-codex: ## Quita de Codex el wiring de Cauce, conservando lo tuyo
84
+ @node tools/ops.js automation uninstall . codex
85
+
86
+ uninstall-gemini: ## Quita de Gemini el wiring de Cauce, conservando lo tuyo
87
+ @node tools/ops.js automation uninstall . gemini
88
+
89
+ uninstall-antigravity: ## Quita de Antigravity el wiring de Cauce, conservando lo tuyo
90
+ @node tools/ops.js automation uninstall . antigravity
91
+
76
92
  doctor-claude: ## Diagnostica la instalación de Claude
77
93
  @node tools/ops.js automation doctor . claude
78
94
 
@@ -1,22 +1,29 @@
1
- # Empresa — {{PROJECT_NAME}}
1
+ # Organización — {{PROJECT_NAME}}
2
2
 
3
- Este archivo es específico de la empresa. Completar hechos; marcar lo desconocido como `Por definir`.
3
+ Este archivo es el contexto estable de quien construye. Completar hechos; marcar lo desconocido como
4
+ `Por definir`. Una empresa, un equipo interno y un proyecto libre lo llenan distinto: lo que no aplica
5
+ se dice, no se inventa.
4
6
 
5
- ## Misión y visión
7
+ ## De qué se trata
6
8
 
7
9
  Por completar.
8
10
 
9
- ## Modelo de negocio
11
+ ## A quién sirve
10
12
 
11
- - Clientes y usuarios:
12
- - Quién paga:
13
- - Propuesta de valor:
14
- - Fuentes de ingreso:
13
+ - Usuarios:
14
+ - Quién decide que se use:
15
+ - Qué mejora para ellos:
16
+
17
+ ## Cómo se sostiene
18
+
19
+ Venta, suscripción, donación, presupuesto interno, trabajo voluntario o una mezcla. Lo que corresponda:
20
+
21
+ - Origen de los recursos:
15
22
  - Costos o restricciones relevantes:
16
23
 
17
24
  ## Objetivos actuales
18
25
 
19
- Por completar. Incluir horizonte, responsable y métrica cuando existan.
26
+ Por completar. Incluir horizonte, responsable y cómo se va a notar.
20
27
 
21
28
  ## Estructura y derechos de decisión
22
29