@ingeniomaps/cauce 0.28.0 → 0.30.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 +79 -0
- package/README.md +27 -5
- package/automatization/runners/antigravity/skills/onboard/SKILL.md +22 -8
- package/automatization/runners/claude/CLAUDE.md +10 -2
- package/automatization/runners/codex/AGENTS.md +12 -4
- package/automatization/runners/gemini/GEMINI.md +12 -4
- package/automatization/workflows/onboard.js +124 -161
- package/engine/cli/args.js +2 -0
- package/engine/cli/ops.js +111 -11
- package/engine/core/onboarding.js +67 -0
- package/engine/core/scan.js +143 -0
- package/package.json +1 -1
- package/template/organization/company.md +16 -9
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,85 @@ 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.30.0] - 2026-08-18
|
|
18
|
+
|
|
19
|
+
### Añadido
|
|
20
|
+
|
|
21
|
+
- **`ops onboard`: qué le falta a una instancia para arrancar, y con qué pregunta empezar.** Determinista
|
|
22
|
+
y en milisegundos: dice si la instancia sigue vacía, qué hay en el workspace y qué queda por cubrir.
|
|
23
|
+
`init` lo imprime al terminar, que es donde mira quien acaba de instalar y todavía no sabe qué hace la
|
|
24
|
+
herramienta. Cuando `organization/` o el roadmap ya tienen contenido real no ofrece nada, porque habría
|
|
25
|
+
trabajo que pisar.
|
|
26
|
+
|
|
27
|
+
### Cambiado
|
|
28
|
+
|
|
29
|
+
- **El arranque pregunta en vez de negarse, y pregunta primero.** Invocado sin contexto, `/onboard`
|
|
30
|
+
terminaba diciendo «volvé a correrlo con contexto» —y gastaba un subagente para decirlo—. Ahora la
|
|
31
|
+
primera línea es la pregunta, sea el workspace vacío, un monorepo o diez repos, y el inventario viene
|
|
32
|
+
después: de qué trata el proyecto no depende de lo que haya en el disco. Cada runner recibe la
|
|
33
|
+
instrucción de abrir con esa pregunta antes de mirar nada.
|
|
34
|
+
|
|
35
|
+
- **El recorrido tiene techo: una llamada cuando falta contexto, tres cuando hay con qué escribir.** Y
|
|
36
|
+
ninguna sale a explorar —tiene prohibido recorrer directorios, leer código fuente y abrir archivos que
|
|
37
|
+
no vaya a escribir—, porque lo que necesita ya está resuelto: un arranque que te hace esperar diez
|
|
38
|
+
minutos dejó de ser un arranque.
|
|
39
|
+
|
|
40
|
+
- **El escaneo mira las raíces declaradas en `ops.config.json`, y nada por encima.** Antes suponía la
|
|
41
|
+
carpeta madre en modo sidecar; ahora acotar `workspaceRoots` acota también lo que se lee, que es más
|
|
42
|
+
rápido y es lo único que alguien autorizó.
|
|
43
|
+
|
|
44
|
+
- **Y saltea lo que tu proyecto ya declaró basura.** Además de los directorios de paquetes y de los
|
|
45
|
+
ocultos, lee el `.gitignore` de cada raíz y omite los nombres de directorio que encuentre ahí. Los
|
|
46
|
+
interpreta de forma ancha —por nombre, a cualquier profundidad, salteando todo patrón con comodín—:
|
|
47
|
+
alcanza para decidir si vale la pena mirar adentro, y no es un parser de gitignore. Las listas largas
|
|
48
|
+
se recortan a veinte en pantalla diciendo cuántas quedaron afuera; `--json` las trae todas.
|
|
49
|
+
|
|
50
|
+
- **El arranque declara para qué es.** Entender qué es el proyecto, dejar la instancia correcta para él y
|
|
51
|
+
que la primera tarea pueda empezar: eso, y nada más. El análisis profundo llega cuando alguien pide
|
|
52
|
+
algo concreto, y adelantarlo retrasa el único momento en que la herramienta todavía no sirve para nada.
|
|
53
|
+
Está escrito en cada prompt del recorrido y en las instrucciones de los cuatro runners.
|
|
54
|
+
|
|
55
|
+
- **Las preguntas dejan de ser un formulario, y de dar por sentado que el proyecto vende algo.** Antes
|
|
56
|
+
eran cuatro fijas, y la primera preguntaba qué vende la empresa y a quién: a un proyecto libre, interno
|
|
57
|
+
o sin fines de lucro le pedían una respuesta que nadie había dado. Ahora hay una sola pregunta escrita
|
|
58
|
+
—de qué trata el proyecto, la única que no depende de ninguna respuesta— y lo que el motor fija después
|
|
59
|
+
son las dimensiones a cubrir: a quién sirve, cómo se sostiene, qué querés que pase, qué está fuera de
|
|
60
|
+
alcance, qué externos hay que conectar. Quien conduce la conversación las formula con las palabras de
|
|
61
|
+
ese proyecto, una por vez y hasta tres.
|
|
62
|
+
|
|
63
|
+
- **El molde de `organization/company.md` deja de asumir un negocio.** «Modelo de negocio», «quién paga»
|
|
64
|
+
y «fuentes de ingreso» pasan a ser «De qué se trata», «A quién sirve» y «Cómo se sostiene», que nombra
|
|
65
|
+
donación, presupuesto interno y trabajo voluntario junto con la venta. Lo que no aplica se dice, no se
|
|
66
|
+
completa con algo plausible. Afecta a las instancias nuevas: `upgrade` sólo reemplaza `system/`, así
|
|
67
|
+
que el archivo que ya escribiste sigue siendo tuyo.
|
|
68
|
+
|
|
69
|
+
## [0.29.0] - 2026-08-18
|
|
70
|
+
|
|
71
|
+
### Añadido
|
|
72
|
+
|
|
73
|
+
- **`ops scan`: qué hay en el workspace, resuelto por código.** Lista los subproyectos con manifiesto
|
|
74
|
+
propio, su runtime y los comandos de test, lint y build que cada uno declara, diciendo de qué archivo
|
|
75
|
+
salió cada comando. No corre ninguno y no inventa ninguno: un comando que nadie declaró se lee igual
|
|
76
|
+
que uno real, y el primer Verify de una tarea es donde eso se descubre. Saltea `node_modules` y todo
|
|
77
|
+
directorio oculto, que es lo que lo mantiene en milisegundos. Una instancia sidecar escanea su carpeta
|
|
78
|
+
madre, donde vive el código; cualquier otro modo, donde está parada.
|
|
79
|
+
|
|
80
|
+
### Cambiado
|
|
81
|
+
|
|
82
|
+
- **`/onboard` cuesta lo que encuentra.** Empezaba pidiéndole a un agente que explorara el repositorio y
|
|
83
|
+
terminaba corriendo la suite de tests de cada servicio: una corrida sobre una carpeta vacía gastó doce
|
|
84
|
+
minutos sin poder producir nada. Ahora arranca con `ops scan` —una llamada, milisegundos— y, si el
|
|
85
|
+
workspace no tiene código y nadie aportó contexto, termina ahí diciendo qué le falta, en vez de escribir
|
|
86
|
+
una empresa inventada.
|
|
87
|
+
|
|
88
|
+
El recorrido ya no ejecuta nada del proyecto. El mapa real anota cada comando **tal como está
|
|
89
|
+
declarado**, con su archivo de origen, y verificarlo corriéndolo pasa a ser una historia de la épica
|
|
90
|
+
001, donde tiene dueño y tiempo asignado. Lo que escribe y lo que deja a una persona no cambió:
|
|
91
|
+
borradores marcados como supuestos, credenciales y MCP en `HUMAN_ACTIONS.md`, épica sin promover.
|
|
92
|
+
|
|
93
|
+
Si ya tenés una instancia, el recorrido actualizado llega con `automation install`, no con `upgrade`:
|
|
94
|
+
los workflows viven en el runner.
|
|
95
|
+
|
|
17
96
|
## [0.28.0] - 2026-08-18
|
|
18
97
|
|
|
19
98
|
### Añadido
|
package/README.md
CHANGED
|
@@ -41,7 +41,24 @@ instala la dependencia, deja el wiring del runner puesto y valida la instancia a
|
|
|
41
41
|
· npm install (el motor viene de la dependencia)
|
|
42
42
|
✓ claude: adaptador operativo (0 advertencia(s))
|
|
43
43
|
✓ planning válido: 0 épica(s), 0 tarea(s) en cola, 0 terminada(s)
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
2 servicio(s) en /home/vos/mi-repo: apps/api, apps/web
|
|
46
|
+
|
|
47
|
+
¿De qué trata este proyecto? Una línea alcanza.
|
|
48
|
+
|
|
49
|
+
Según lo que contestes salen hasta 3 preguntas más, con las palabras de
|
|
50
|
+
este proyecto, hasta cubrir lo que haga falta de esto:
|
|
51
|
+
|
|
52
|
+
· a quién sirve y quién lo usa
|
|
53
|
+
· cómo se sostiene: venta, suscripción, donación, presupuesto interno o trabajo voluntario
|
|
54
|
+
· qué querés que pase en este período y cómo se va a notar
|
|
55
|
+
· qué servicios o carpetas están muertos o fuera de alcance
|
|
56
|
+
· qué sistema externo o MCP hace falta conectar, y contra qué entorno
|
|
57
|
+
|
|
58
|
+
Mientras tanto, esto es lo que hay: apps/api, apps/web
|
|
59
|
+
|
|
60
|
+
Abrí claude en este directorio para que las escriba por vos.
|
|
61
|
+
El ciclo empieza en ops/planning/FLOW.md.
|
|
45
62
|
```
|
|
46
63
|
|
|
47
64
|
El default de las dos preguntas es no hacer nada: instalar un runner escribe en tu repositorio y
|
|
@@ -101,10 +118,13 @@ vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es l
|
|
|
101
118
|
no puede hacer, así que ese recorrido vive en el runner:
|
|
102
119
|
|
|
103
120
|
```text
|
|
104
|
-
/onboard
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
/onboard te pregunta de qué trata el proyecto y, según lo que contestes, hasta tres más
|
|
122
|
+
con las palabras de ese proyecto —no un formulario que da por sentado que vendés
|
|
123
|
+
algo—. Con tus respuestas escribe organization/, el «Mapa real» de AGENTS.md y
|
|
124
|
+
las raíces de ops.config.json.
|
|
125
|
+
Lo deducido queda marcado como supuesto; credenciales, MCP y el permiso de push van
|
|
126
|
+
a HUMAN_ACTIONS.md. Cierra con la épica 001, sin promoverla. No corre nada del
|
|
127
|
+
proyecto: verificar los comandos es una historia de esa épica.
|
|
108
128
|
/team evalúa si una intención posterior es viable y propone su épica.
|
|
109
129
|
/autobuild ejecuta una tarea ya promovida, fase por fase.
|
|
110
130
|
```
|
|
@@ -141,6 +161,8 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
|
|
|
141
161
|
| Comando | Función |
|
|
142
162
|
|---|---|
|
|
143
163
|
| `ops init [destino]` | Materializa una instancia y la deja usable; sin destino, en `ops/` y modo sidecar. |
|
|
164
|
+
| `ops scan [workspace]` | Inventaría servicios y comandos declarados, sin correr ninguno. |
|
|
165
|
+
| `ops onboard [ops-root]` | Dice qué falta para arrancar y con qué pregunta empezar. |
|
|
144
166
|
| `ops check <planning>` | Valida contratos, unicidad, trazabilidad y estados. |
|
|
145
167
|
| `ops tree <planning>` | Muestra roadmap, backlog, WIP, inbox y done sin mutar nada. |
|
|
146
168
|
| `ops context <planning>` | Emite el contexto mínimo de la tarea vigente para un runner. |
|
|
@@ -4,15 +4,25 @@ 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.
|
|
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.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
16
26
|
|
|
17
27
|
Con eso escribí `{{OPS_DIR}}organization/company.md` y `product.md`, la sección «Mapa real» de
|
|
18
28
|
`{{OPS_DIR}}AGENTS.md` con el resultado que obtuviste por comando, y las raíces reales en
|
|
@@ -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,
|
|
11
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
// Arranque de una instancia recién creada: convierte un repositorio que nadie le explicó al toolkit en
|
|
2
2
|
// contexto escrito —`organization/`, el mapa real de `AGENTS.md`, las raíces de código— y en la primera
|
|
3
|
-
// épica.
|
|
4
|
-
// qué es el producto, qué carpeta es legacy y cuál es el comando que de verdad lo valida, no lo es.
|
|
3
|
+
// épica.
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// El orden importa y se pagó caro: la primera versión le pedía a un agente que «inventariara el
|
|
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 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.
|
|
14
|
+
//
|
|
15
|
+
// Escribe borradores y no decide por nadie: lo deducido queda marcado como supuesto, las credenciales y
|
|
16
|
+
// los sistemas externos van a HUMAN_ACTIONS —R12 se los prohíbe a un runner— y la épica queda sin
|
|
17
|
+
// promover, que sigue siendo la firma humana.
|
|
9
18
|
export const meta = {
|
|
10
19
|
name: 'onboard',
|
|
11
|
-
description: '
|
|
20
|
+
description: 'Inventaría el workspace y deja escrito el contexto de la empresa y la primera épica',
|
|
12
21
|
whenToUse: 'Primera corrida después de "cauce init", cuando organization/ y el roadmap están vacíos.',
|
|
13
22
|
phases: [
|
|
14
|
-
{ title: 'Scan', detail: '
|
|
15
|
-
{ title: '
|
|
16
|
-
{ title: '
|
|
17
|
-
{ title: 'Epic', detail: 'La épica que deja al ciclo poder correr' },
|
|
18
|
-
{ 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' },
|
|
19
26
|
],
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
// El prefijo lo completa `automation install
|
|
23
|
-
//
|
|
29
|
+
// El prefijo lo completa `automation install`: el runtime no expone `process`, así que la ruta de la
|
|
30
|
+
// raíz ops viaja escrita, relativa a donde se abre la herramienta.
|
|
24
31
|
const ROOT = '{{OPS_DIR}}'.replace(/\/+$/, '') || '.'
|
|
25
32
|
const P = `${ROOT}/planning`
|
|
26
33
|
const ORG = `${ROOT}/organization`
|
|
@@ -28,18 +35,25 @@ const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
|
28
35
|
const INBOX = `${P}/INBOX.md`
|
|
29
36
|
const ROADMAP = `${P}/roadmap`
|
|
30
37
|
|
|
31
|
-
// Lo que la persona ya sabe y no
|
|
32
|
-
// Entra como
|
|
38
|
+
// Lo que la persona ya sabe y el repositorio no puede decir: `/onboard vendemos ruteo a PYMEs de
|
|
39
|
+
// logística`. Entra como hecho; lo que se deduce, no.
|
|
33
40
|
const input = typeof args === 'string' ? { context: args } : (args || {})
|
|
34
41
|
const CONTEXT = String(input.context || '').trim()
|
|
35
42
|
const FORCE = Boolean(input.force)
|
|
36
43
|
|
|
37
|
-
const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsables. Distinguí
|
|
38
|
-
`
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
`
|
|
42
|
-
`
|
|
44
|
+
const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsables. Distinguí lo que leíste ` +
|
|
45
|
+
`en un archivo de lo que estás suponiendo: lo segundo va marcado "(supuesto)" en el texto que escribas. ` +
|
|
46
|
+
`No leas archivos de credenciales —.env, *.pem, claves— ni copies su contenido a ningún lado; ` +
|
|
47
|
+
`.env.example sí, y sólo los nombres de las variables. No corras comandos del proyecto: este recorrido ` +
|
|
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.`
|
|
43
57
|
|
|
44
58
|
function finish(result) {
|
|
45
59
|
log(`Fin: ${JSON.stringify(result)}`)
|
|
@@ -51,50 +65,34 @@ const stop = (reason, detail = '') => {
|
|
|
51
65
|
return finish({ stopped: true, reason, detail })
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
const
|
|
55
|
-
type: 'object', additionalProperties: false, required: ['fresh'],
|
|
68
|
+
const SCAN = {
|
|
69
|
+
type: 'object', additionalProperties: false, required: ['fresh', 'services'],
|
|
56
70
|
properties: {
|
|
57
71
|
fresh: { type: 'boolean' },
|
|
58
72
|
reason: { type: 'string' },
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const INVENTORY = {
|
|
65
|
-
type: 'object', additionalProperties: false, required: ['services'],
|
|
66
|
-
properties: {
|
|
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' } },
|
|
67
78
|
services: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
68
|
-
required: ['path'
|
|
69
|
-
path: { type: 'string' },
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
required: ['path'], properties: {
|
|
80
|
+
path: { type: 'string' },
|
|
81
|
+
runtimes: { type: 'array', items: { type: 'string' } },
|
|
82
|
+
// Comando declarado y de qué archivo salió. Verificar que además corra es una historia de la
|
|
83
|
+
// épica: correr la suite de cada servicio acá convertía el arranque en una espera larga.
|
|
84
|
+
commands: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
85
|
+
required: ['kind', 'command', 'source'], properties: {
|
|
86
|
+
kind: { type: 'string' }, command: { type: 'string' }, source: { type: 'string' },
|
|
87
|
+
} } },
|
|
72
88
|
} } },
|
|
73
|
-
legacy: { type: 'array', items: { type: 'string' } },
|
|
74
|
-
ci: { type: 'array', items: { type: 'string' } },
|
|
75
89
|
externals: { type: 'array', items: { type: 'string' } },
|
|
76
90
|
secrets: { type: 'array', items: { type: 'string' } },
|
|
77
|
-
productHints: { type: 'string' },
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const CHECKED = {
|
|
82
|
-
type: 'object', additionalProperties: false, required: ['path', 'results'],
|
|
83
|
-
properties: {
|
|
84
|
-
path: { type: 'string' },
|
|
85
|
-
results: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
86
|
-
required: ['kind', 'command', 'status'], properties: {
|
|
87
|
-
kind: { type: 'string', enum: ['test', 'lint', 'build'] },
|
|
88
|
-
command: { type: 'string' },
|
|
89
|
-
// `ausente` es un resultado, no un fallo: un servicio sin lint declarado no tiene nada roto.
|
|
90
|
-
status: { type: 'string', enum: ['verificado', 'falla', 'ausente'] },
|
|
91
|
-
detail: { type: 'string' },
|
|
92
|
-
} } },
|
|
93
91
|
},
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
const WRITTEN = {
|
|
97
|
-
type: 'object', additionalProperties: false, required: ['files'
|
|
95
|
+
type: 'object', additionalProperties: false, required: ['files'],
|
|
98
96
|
properties: {
|
|
99
97
|
files: { type: 'array', items: { type: 'string' } },
|
|
100
98
|
assumptions: { type: 'array', items: { type: 'string' } },
|
|
@@ -105,150 +103,115 @@ const WRITTEN = {
|
|
|
105
103
|
|
|
106
104
|
phase('Scan')
|
|
107
105
|
|
|
108
|
-
//
|
|
109
|
-
//
|
|
106
|
+
// Una sola llamada, y todo lo que hace es correr dos comandos y mirar dos archivos. Lo que sigue depende
|
|
107
|
+
// de lo que devuelva, así que gastar más antes de saberlo es gastar a ciegas.
|
|
110
108
|
const state = await agent(
|
|
111
|
-
`${BASE}\n\nFrom
|
|
112
|
-
|
|
113
|
-
`
|
|
114
|
-
`
|
|
115
|
-
`
|
|
116
|
-
|
|
109
|
+
`${BASE}\n\nFrom ${ROOT}, run exactly these two commands and report what they printed. Explore nothing ` +
|
|
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` +
|
|
115
|
+
`2. "node tools/ops.js check planning".\n` +
|
|
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.`,
|
|
118
|
+
{ schema: SCAN, label: 'inventario' },
|
|
117
119
|
)
|
|
118
|
-
if (!state) return stop('
|
|
120
|
+
if (!state) return stop('scan-unavailable', 'no se pudo leer el estado del workspace')
|
|
119
121
|
if (!state.fresh && !FORCE) {
|
|
120
122
|
return stop('ya-arrancado', `${state.reason || 'la instancia ya tiene contexto escrito'}. ` +
|
|
121
123
|
`Pasá force:true si querés reescribir los borradores.`)
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`go.mod, pyproject.toml, Cargo.toml, composer.json, pom.xml, Gemfile, Makefile, Dockerfile o ` +
|
|
128
|
-
`docker-compose— reportá su ruta relativa, el runtime, para qué parece servir, y los comandos de test, ` +
|
|
129
|
-
`lint y build que el propio proyecto declara, con source apuntando al archivo y la clave de donde los ` +
|
|
130
|
-
`sacaste. Un comando que no está declarado se omite: no lo adivines.\n\n` +
|
|
131
|
-
`Reportá además qué directorios parecen legacy o fuera de alcance, qué corre en CI, qué servicios ` +
|
|
132
|
-
`externos aparecen nombrados en configuración o dependencias, y qué credenciales espera el proyecto ` +
|
|
133
|
-
`—sólo los nombres de variable, leídos de .env.example o de la configuración de CI—. En productHints ` +
|
|
134
|
-
`resumí lo que el repositorio deja ver sobre qué se construye.` +
|
|
135
|
-
`${CONTEXT ? `\n\nLa persona ya aportó este contexto, que vale como hecho: ${CONTEXT}` : ''}`,
|
|
136
|
-
{ schema: INVENTORY, label: 'inventario' },
|
|
137
|
-
)
|
|
138
|
-
if (!inventory) return stop('inventario-vacio', 'el escaneo no devolvió resultado')
|
|
139
|
-
const services = inventory.services || []
|
|
140
|
-
|
|
141
|
-
// Un workspace sin código no es un error: alguien puede estar preparando la carpeta antes de clonar los
|
|
142
|
-
// repos, y `init` en un directorio vacío es un arranque legítimo. Lo que no puede es terminar sin nada
|
|
143
|
-
// escrito: lo que sí se pueda establecer se escribe igual, y traer el código pasa a ser la primera
|
|
144
|
-
// historia en vez de un checkpoint que no deja nada.
|
|
145
|
-
const VACIO = services.length ? '' : `\n\nNo hay ningún subproyecto con manifiesto propio en el ` +
|
|
146
|
-
`workspace: el código todavía no está acá. Escribí igual lo que el contexto aportado permita, dejá el ` +
|
|
147
|
-
`mapa real declarado como pendiente diciendo qué lo completa, y que la primera historia de la épica sea ` +
|
|
148
|
-
`traer los repos y declararlos en workspaceRoots.`
|
|
149
|
-
log(services.length
|
|
150
|
-
? `${services.length} servicio(s): ${services.map((service) => service.path).join(', ')}`
|
|
151
|
-
: 'Sin servicios en el workspace: el arranque escribe lo que se pueda y deja el mapa pendiente.')
|
|
152
|
-
|
|
153
|
-
phase('Verify')
|
|
126
|
+
const services = state.services || []
|
|
127
|
+
const listado = services.map((service) => service.path).join(', ')
|
|
128
|
+
log(`${services.length} servicio(s) en el workspace${listado ? `: ${listado}` : ''}`)
|
|
154
129
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
`
|
|
164
|
-
`
|
|
165
|
-
|
|
166
|
-
|
|
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 })
|
|
145
|
+
}
|
|
167
146
|
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
147
|
+
const INVENTARIO = { services, externals: state.externals || [], secrets: state.secrets || [] }
|
|
148
|
+
const EVIDENCE = `Inventario del workspace:\n${JSON.stringify(INVENTARIO)}` +
|
|
149
|
+
`${CONTEXT ? `\n\nContexto aportado por la persona, que vale como hecho: ${CONTEXT}` : ''}` +
|
|
150
|
+
`${services.length ? '' : '\n\nNo hay ningún servicio en el workspace: el código todavía no está acá.'}`
|
|
171
151
|
|
|
172
152
|
phase('Draft')
|
|
173
153
|
|
|
174
|
-
const EVIDENCE = `Inventario:\n${JSON.stringify(inventory)}\n\n` +
|
|
175
|
-
`Comandos comprobados:\n${JSON.stringify(checks)}${VACIO}` +
|
|
176
|
-
`${CONTEXT ? `\n\nContexto aportado por la persona, que vale como hecho: ${CONTEXT}` : ''}`
|
|
177
|
-
|
|
178
154
|
const drafted = await agent(
|
|
179
|
-
`${BASE}\n\n${EVIDENCE}\n\nEscribí
|
|
180
|
-
`
|
|
181
|
-
`1. ${ORG}/company.md y ${ORG}/product.md: lo que
|
|
182
|
-
`afirmar. Lo
|
|
183
|
-
`
|
|
184
|
-
`
|
|
185
|
-
`
|
|
186
|
-
`
|
|
187
|
-
`
|
|
188
|
-
`
|
|
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` +
|
|
161
|
+
`2. La sección "## Mapa real" de ${ROOT}/AGENTS.md: una entrada por servicio con su ruta, su runtime y ` +
|
|
162
|
+
`sus comandos **tal como los declara**, diciendo de qué archivo salió cada uno. No afirmes que ` +
|
|
163
|
+
`funcionan: nadie los corrió. Un servicio sin comandos declarados se escribe así, que es información.\n` +
|
|
164
|
+
`3. ${ROOT}/ops.config.json: dejá en workspaceRoots las raíces de código reales, que es lo que un guard ` +
|
|
165
|
+
`usa para bloquear una escritura fuera de lugar. Una raíz de más lo apaga.\n` +
|
|
166
|
+
`${services.length ? '' : 'Sin servicios, el mapa queda declarado como pendiente, diciendo qué lo ' +
|
|
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` +
|
|
189
173
|
`Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
|
|
190
174
|
{ schema: WRITTEN, label: 'contexto' },
|
|
191
175
|
)
|
|
192
176
|
if (!drafted) return stop('draft-unavailable', 'los borradores no devolvieron resultado')
|
|
193
177
|
|
|
194
|
-
// Credenciales, MCP y permiso de push no son trabajo del runner: R12 se los prohíbe y R13 exige dejar
|
|
195
|
-
// dicho quién los resuelve y con qué. La fila vale más que la negativa.
|
|
196
|
-
const pending = await agent(
|
|
197
|
-
`${BASE}\n\n${EVIDENCE}\n\nRegistrá en ${HUMAN} una fila por cada cosa que necesita a una persona, con ` +
|
|
198
|
-
`la tarea, el estado pendiente, el origen "onboard" y la acción concreta que la desbloquea. Como mínimo, ` +
|
|
199
|
-
`una por cada credencial que el proyecto espera —diciendo dónde se cargan en este proyecto y quién lo ` +
|
|
200
|
-
`hace, sin proponer ningún valor—, una por cada servicio externo o MCP a conectar —con su alcance y ` +
|
|
201
|
-
`contra qué entorno—, y una por la autoridad del runner: hoy ops.config.json declara ` +
|
|
202
|
-
`runner.allowPush=false y cambiarlo es una decisión humana. Las preguntas que quedaron abiertas van a ` +
|
|
203
|
-
`la sección Ideas de ${INBOX}, sin promover: ${JSON.stringify(drafted.openQuestions || [])}`,
|
|
204
|
-
{ schema: WRITTEN, label: 'acciones-humanas' },
|
|
205
|
-
)
|
|
206
|
-
|
|
207
178
|
phase('Epic')
|
|
208
179
|
|
|
209
180
|
const epic = await agent(
|
|
210
|
-
`${BASE}\n\n${EVIDENCE}\n\nSupuestos que quedaron escritos: ${JSON.stringify(drafted.assumptions || [])}\n` +
|
|
211
|
-
`Comandos que fallaron: ${JSON.stringify(broken)}\n\n` +
|
|
181
|
+
`${BASE}\n\n${EVIDENCE}\n\nSupuestos que quedaron escritos: ${JSON.stringify(drafted.assumptions || [])}\n\n` +
|
|
212
182
|
`Escribí en ${ROADMAP} la épica epic-001-<slug>.md siguiendo el contrato de ${P}/PROTOCOL.md: ` +
|
|
213
183
|
`frontmatter epic/title/status/service con status open, criterios **CN** observables, "## Contexto ` +
|
|
214
|
-
`relevante" con rutas
|
|
215
|
-
`
|
|
184
|
+
`relevante" con rutas reales e historias con (→ CN) y (service: ruta), cada una de menos de cuatro ` +
|
|
185
|
+
`horas.\n\n` +
|
|
216
186
|
`Su resultado es que una tarea pueda atravesar el ciclo entero sin que nadie tenga que volver a ` +
|
|
217
|
-
`explicar este proyecto. Los criterios salen de lo que hoy falta y son verificables: que ` +
|
|
218
|
-
`
|
|
219
|
-
`
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
187
|
+
`explicar este proyecto. Los criterios salen de lo que hoy falta y son verificables: que organization/ ` +
|
|
188
|
+
`no tenga supuestos sin confirmar, que cada comando del mapa esté verificado corriéndolo y anotado con ` +
|
|
189
|
+
`su resultado, que las raíces declaradas hagan que el guard de límites bloquee una escritura afuera y ` +
|
|
190
|
+
`deje pasar una adentro, y que una tarea piloto real llegue a DONE con evidencia. ` +
|
|
191
|
+
`${services.length
|
|
192
|
+
? 'Verificar los comandos es una historia: nadie los corrió todavía.'
|
|
193
|
+
: 'La primera historia es traer los repos y declararlos en workspaceRoots.'} ` +
|
|
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'],
|
|
224
199
|
properties: {
|
|
225
|
-
file: { type: 'string' },
|
|
200
|
+
file: { type: 'string' }, passed: { type: 'boolean' }, details: { type: 'string' },
|
|
226
201
|
criteria: { type: 'array', items: { type: 'string' } },
|
|
227
202
|
stories: { type: 'array', items: { type: 'string' } },
|
|
228
203
|
} }, label: 'epica-001' },
|
|
229
204
|
)
|
|
230
205
|
if (!epic) return stop('epic-unavailable', 'la épica no devolvió resultado')
|
|
231
|
-
|
|
232
|
-
phase('Closing')
|
|
233
|
-
|
|
234
|
-
const closing = await agent(
|
|
235
|
-
`${BASE}\n\nFrom ${ROOT}, run "node tools/ops.js check planning" and report whether it passed. If it ` +
|
|
236
|
-
`failed, repair only what this run wrote —the epic, the config— so it satisfies the contract; never ` +
|
|
237
|
-
`weaken a criterion to force green.`,
|
|
238
|
-
{ schema: { type: 'object', additionalProperties: false, required: ['passed', 'details'],
|
|
239
|
-
properties: { passed: { type: 'boolean' }, details: { type: 'string' } } }, label: 'closing-check' },
|
|
240
|
-
)
|
|
241
|
-
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')
|
|
242
207
|
|
|
243
208
|
const supuestos = (drafted.assumptions || []).length
|
|
244
|
-
const acciones = (
|
|
209
|
+
const acciones = (drafted.humanActions || []).length
|
|
245
210
|
log(`Contexto escrito con ${supuestos} supuesto(s) por confirmar y ${acciones} acción(es) humana(s) en ${HUMAN}.`)
|
|
246
211
|
log(`Épica en ${epic.file}, sin promover: revisala, promoví una historia a un hito del BACKLOG y corré /autobuild.`)
|
|
247
212
|
|
|
248
213
|
return finish({
|
|
249
214
|
services: services.length,
|
|
250
|
-
verified: green,
|
|
251
|
-
broken: broken.length,
|
|
252
215
|
assumptions: supuestos,
|
|
253
216
|
humanActions: acciones,
|
|
254
217
|
epic: epic.file,
|
package/engine/cli/args.js
CHANGED
|
@@ -14,6 +14,8 @@ const VALUED_FLAGS = new Set([
|
|
|
14
14
|
// —un agente, típicamente— recibía texto sin ninguna señal de que su bandera no existía.
|
|
15
15
|
const FLAGS = {
|
|
16
16
|
init: ['--name', '--mode', '--force', '--runner', '--integration', '--install', '--no-install'],
|
|
17
|
+
scan: ['--json'],
|
|
18
|
+
onboard: ['--json'],
|
|
17
19
|
check: ['--json'],
|
|
18
20
|
tree: ['--json', '--no-color'],
|
|
19
21
|
context: ['--json'],
|
package/engine/cli/ops.js
CHANGED
|
@@ -14,6 +14,8 @@ const F = require('../core/files')
|
|
|
14
14
|
const O = require('../core/ownership')
|
|
15
15
|
const CL = require('../core/changelog')
|
|
16
16
|
const M = require('../core/manifest')
|
|
17
|
+
const SC = require('../core/scan')
|
|
18
|
+
const OB = require('../core/onboarding')
|
|
17
19
|
const C = require('../config/validate')
|
|
18
20
|
const T = require('../teams/registry')
|
|
19
21
|
const AG = require('../agents/catalog')
|
|
@@ -26,6 +28,10 @@ const PROJECT_ROOT = path.resolve(__dirname, '..', '..')
|
|
|
26
28
|
// Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
|
|
27
29
|
const DEFAULT_TARGET = 'ops'
|
|
28
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
|
+
|
|
29
35
|
function fail(message, code = 1) {
|
|
30
36
|
console.error(message)
|
|
31
37
|
process.exit(code)
|
|
@@ -35,6 +41,8 @@ function usage() {
|
|
|
35
41
|
console.log(`Uso:
|
|
36
42
|
ops init [destino] [--name <nombre>] [--mode embedded|sidecar] [--force]
|
|
37
43
|
[--runner claude|codex|gemini|antigravity] [--integration <proveedor>] [--install|--no-install]
|
|
44
|
+
ops scan [workspace] [--json]
|
|
45
|
+
ops onboard [ops-root] [--json]
|
|
38
46
|
ops check <planning-dir> [--json]
|
|
39
47
|
ops tree <planning-dir> [--no-color] [--json]
|
|
40
48
|
ops context <planning-dir> [--json]
|
|
@@ -316,22 +324,112 @@ async function init(target, cli) {
|
|
|
316
324
|
})
|
|
317
325
|
} catch (error) { fail(error.message, 2) }
|
|
318
326
|
|
|
319
|
-
if (resultado.instalado)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
if (resultado.instalado) check(path.join(root, 'planning'), SIN_BANDERAS)
|
|
328
|
+
|
|
329
|
+
// Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el molde
|
|
330
|
+
// y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es justo
|
|
331
|
+
// lo que un CLI determinista no puede hacer; lo que sí puede es decir qué falta y qué preguntar.
|
|
332
|
+
//
|
|
333
|
+
// Se imprime siempre, incluso cuando la dependencia no se instaló: lo resuelve el motor que está
|
|
334
|
+
// corriendo `init`, no cuesta nada, y es lo único que le dice a alguien qué hacer con lo que acaba de
|
|
335
|
+
// crear. Dejarlo adentro del camino feliz lo escondía justo de quien más lo necesita.
|
|
336
|
+
console.log('')
|
|
337
|
+
onboard(root, SIN_BANDERAS)
|
|
338
|
+
if (resultado.instalado && resultado.runner !== BOOT.SIN_RUNNER) {
|
|
339
|
+
console.log(`\nAbrí ${resultado.runner} en este directorio para que las escriba por vos.`)
|
|
330
340
|
}
|
|
341
|
+
console.log('')
|
|
331
342
|
for (const paso of initSteps(enter, resultado)) console.log(paso)
|
|
343
|
+
if (resultado.instalado) {
|
|
344
|
+
console.log(`El ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}.`)
|
|
345
|
+
}
|
|
332
346
|
if (resultado.error) fail(`${resultado.error}: la instancia quedó creada pero todavía no funciona.`)
|
|
333
347
|
}
|
|
334
348
|
|
|
349
|
+
// Dónde puede mirar una instancia: exactamente las raíces que declara, y nada por encima de ellas. Sale
|
|
350
|
+
// de `ops.config.json` en vez de suponerse —el sidecar declara `..`, el embebido `.`— para que acotar las
|
|
351
|
+
// raíces acote también el escaneo, y para que nadie termine recorriendo la carpeta de al lado.
|
|
352
|
+
function workspaceRoots(root) {
|
|
353
|
+
try {
|
|
354
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
355
|
+
const declared = (config.workspaceRoots || []).map((entry) => path.resolve(root, entry.path || '.'))
|
|
356
|
+
return declared.length ? declared : [root]
|
|
357
|
+
} catch { return [root] }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Qué hay en las raíces declaradas, antes de que nadie razone sobre ello. La raíz ops se saltea: no es
|
|
361
|
+
// un servicio del proyecto, y su `package.json` sólo declara el motor.
|
|
362
|
+
function inventory(root) {
|
|
363
|
+
const found = []
|
|
364
|
+
for (const workspace of workspaceRoots(root)) {
|
|
365
|
+
const result = SC.scan(workspace, root)
|
|
366
|
+
if (result.rootManifests.length) {
|
|
367
|
+
found.push({ path: '.', root: workspace, runtimes: ['raíz'], commands: result.rootCommands })
|
|
368
|
+
}
|
|
369
|
+
for (const service of result.services) found.push({ ...service, root: workspace })
|
|
370
|
+
}
|
|
371
|
+
return found
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function scan(target, cli) {
|
|
375
|
+
const root = path.resolve(target || '.')
|
|
376
|
+
const result = target
|
|
377
|
+
? { root: path.resolve(target), services: SC.scan(path.resolve(target)).services }
|
|
378
|
+
: { root, services: inventory(root) }
|
|
379
|
+
if (cli.has('--json')) return console.log(JSON.stringify(result, null, 2))
|
|
380
|
+
// Un monorepo de sesenta paquetes no se lee en pantalla. Se recorta, y se dice cuánto: un corte que no
|
|
381
|
+
// se anuncia hace pasar lo listado por todo lo que hay.
|
|
382
|
+
for (const service of result.services.slice(0, LISTA)) {
|
|
383
|
+
const donde = service.root && service.root !== result.root ? `${path.basename(service.root)}/` : ''
|
|
384
|
+
console.log(`${donde}${service.path} [${(service.runtimes || []).join(', ')}]${comandos(service.commands)}`)
|
|
385
|
+
}
|
|
386
|
+
if (result.services.length > LISTA) {
|
|
387
|
+
console.log(`… y ${result.services.length - LISTA} más, todos en --json`)
|
|
388
|
+
}
|
|
389
|
+
console.log(`${result.services.length} candidato(s). Cuál es el producto y cuál quedó muerto lo ` +
|
|
390
|
+
'decide una persona.')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Sólo lo declarado y de dónde salió: un comando inventado se lee igual que uno real.
|
|
394
|
+
function comandos(commands) {
|
|
395
|
+
const entries = Object.entries(commands || {})
|
|
396
|
+
if (!entries.length) return ' — sin comandos declarados'
|
|
397
|
+
return ` — ${entries.map(([kind, value]) => `${kind}: ${value.command} (${value.source})`).join(', ')}`
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// La guía de arranque: qué hay, qué falta y qué preguntar. Determinista y en milisegundos, porque es lo
|
|
401
|
+
// primero que ve alguien que acaba de instalar y todavía no sabe qué hace la herramienta.
|
|
402
|
+
function onboard(rootArg, cli) {
|
|
403
|
+
const root = path.resolve(rootArg || '.')
|
|
404
|
+
const services = inventory(root)
|
|
405
|
+
const state = OB.guide(root, services)
|
|
406
|
+
if (cli.has('--json')) {
|
|
407
|
+
return console.log(JSON.stringify({ ...state, roots: workspaceRoots(root), servicios: services }, null, 2))
|
|
408
|
+
}
|
|
409
|
+
// La pregunta primero, y el inventario después: de qué trata el proyecto es lo mismo esté vacío,
|
|
410
|
+
// sea un monorepo o sean diez repos, y empezar por lo que se encontró invierte de qué se trata esto.
|
|
411
|
+
if (state.fresh) {
|
|
412
|
+
console.log(`${state.opening}\n`)
|
|
413
|
+
console.log(`Según lo que contestes salen hasta ${state.followUps} preguntas más, con las palabras de`)
|
|
414
|
+
console.log('este proyecto, hasta cubrir lo que haga falta de esto:\n')
|
|
415
|
+
for (const dimension of state.dimensions) console.log(` · ${dimension.need}`)
|
|
416
|
+
console.log('')
|
|
417
|
+
}
|
|
418
|
+
const nombres = services.slice(0, LISTA).map((service) => service.path).join(', ')
|
|
419
|
+
const resto = services.length > LISTA ? ` y ${services.length - LISTA} más` : ''
|
|
420
|
+
console.log(services.length
|
|
421
|
+
? `Mientras tanto, esto es lo que hay: ${nombres}${resto}`
|
|
422
|
+
: 'Mientras tanto, en el workspace todavía no hay ningún proyecto.')
|
|
423
|
+
if (!state.fresh) {
|
|
424
|
+
const escrito = [state.written.organization && 'organization/', state.written.roadmap && 'el roadmap']
|
|
425
|
+
.filter(Boolean).join(' y ')
|
|
426
|
+
console.log(`Esta instancia ya tiene ${escrito} escrito: el arranque no la va a pisar.`)
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
console.log('\nCon tus respuestas, el arranque escribe organization/, el mapa real de AGENTS.md y la')
|
|
430
|
+
console.log('primera épica. Con un runner instalado: /onboard, que te las hace una por una.')
|
|
431
|
+
}
|
|
432
|
+
|
|
335
433
|
function check(dir, cli) {
|
|
336
434
|
const root = path.resolve(dir || '.')
|
|
337
435
|
const errors = []
|
|
@@ -1138,6 +1236,8 @@ async function run(cli) {
|
|
|
1138
1236
|
}
|
|
1139
1237
|
const arg = cli.positional
|
|
1140
1238
|
if (command === 'init') await init(arg[1], cli)
|
|
1239
|
+
else if (command === 'scan') scan(arg[1], cli)
|
|
1240
|
+
else if (command === 'onboard') onboard(arg[1], cli)
|
|
1141
1241
|
else if (command === 'check') check(arg[1], cli)
|
|
1142
1242
|
else if (command === 'tree') tree(arg[1], cli)
|
|
1143
1243
|
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 }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Qué hay en el workspace, resuelto por código y no por un modelo. Existe porque el arranque empezaba
|
|
4
|
+
// pidiéndole a un agente que «inventariara el repositorio»: en una carpeta vacía eso gastó doce minutos
|
|
5
|
+
// para no encontrar nada. Recorrer directorios y leer manifiestos es determinista, y lo que no lo es
|
|
6
|
+
// —qué de todo esto es el producto, qué está muerto— recién vale la pena preguntárselo a un modelo
|
|
7
|
+
// cuando esta lista existe.
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs')
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
|
|
12
|
+
// Lo que nunca es un servicio del proyecto. `node_modules` es el que hace la diferencia entre
|
|
13
|
+
// milisegundos y minutos: adentro hay un manifiesto por dependencia. Los directorios ocultos se saltean
|
|
14
|
+
// enteros —regla, no lista—: ahí viven la configuración del runner, el banco de evaluación y las cachés,
|
|
15
|
+
// y un servicio del producto no se esconde detrás de un punto.
|
|
16
|
+
const IGNORED = new Set([
|
|
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',
|
|
20
|
+
])
|
|
21
|
+
|
|
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
|
+
}
|
|
43
|
+
|
|
44
|
+
// Un servicio anidado más hondo que esto es una excepción, y recorrer el árbol entero para encontrarlo
|
|
45
|
+
// cuesta más que declararlo a mano en `AGENTS.md`.
|
|
46
|
+
const DEPTH = 3
|
|
47
|
+
|
|
48
|
+
const MANIFESTS = [
|
|
49
|
+
{ file: 'package.json', runtime: 'node' },
|
|
50
|
+
{ file: 'go.mod', runtime: 'go' },
|
|
51
|
+
{ file: 'pyproject.toml', runtime: 'python' },
|
|
52
|
+
{ file: 'requirements.txt', runtime: 'python' },
|
|
53
|
+
{ file: 'Cargo.toml', runtime: 'rust' },
|
|
54
|
+
{ file: 'composer.json', runtime: 'php' },
|
|
55
|
+
{ file: 'pom.xml', runtime: 'java' },
|
|
56
|
+
{ file: 'build.gradle', runtime: 'java' },
|
|
57
|
+
{ file: 'Gemfile', runtime: 'ruby' },
|
|
58
|
+
{ file: 'Makefile', runtime: 'make' },
|
|
59
|
+
{ file: 'docker-compose.yml', runtime: 'compose' },
|
|
60
|
+
{ file: 'docker-compose.yaml', runtime: 'compose' },
|
|
61
|
+
{ file: 'Dockerfile', runtime: 'docker' },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
// Sólo lo declarado, con su archivo: un comando inventado se lee igual que uno real, y el primer Verify
|
|
65
|
+
// de una tarea es donde se descubre que no existe.
|
|
66
|
+
function npmScripts(file) {
|
|
67
|
+
try {
|
|
68
|
+
const scripts = JSON.parse(fs.readFileSync(file, 'utf8')).scripts || {}
|
|
69
|
+
return ['test', 'lint', 'build'].reduce((found, key) => (
|
|
70
|
+
scripts[key] ? { ...found, [key]: { command: `npm run ${key}`, source: 'package.json' } } : found
|
|
71
|
+
), {})
|
|
72
|
+
} catch { return {} }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeTargets(file) {
|
|
76
|
+
try {
|
|
77
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
78
|
+
return ['test', 'lint', 'build'].reduce((found, key) => (
|
|
79
|
+
new RegExp(`^${key}:`, 'm').test(text)
|
|
80
|
+
? { ...found, [key]: { command: `make ${key}`, source: 'Makefile' } }
|
|
81
|
+
: found
|
|
82
|
+
), {})
|
|
83
|
+
} catch { return {} }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function commandsOf(dir) {
|
|
87
|
+
const packageJson = path.join(dir, 'package.json')
|
|
88
|
+
const makefile = path.join(dir, 'Makefile')
|
|
89
|
+
return {
|
|
90
|
+
// El Makefile gana sobre los scripts cuando los dos existen: el que envuelve al otro es el que el
|
|
91
|
+
// proyecto quiere que se corra.
|
|
92
|
+
...(fs.existsSync(packageJson) ? npmScripts(packageJson) : {}),
|
|
93
|
+
...(fs.existsSync(makefile) ? makeTargets(makefile) : {}),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function manifestsOf(dir) {
|
|
98
|
+
return MANIFESTS.filter((entry) => fs.existsSync(path.join(dir, entry.file)))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Servicios candidatos bajo `root`, sin entrar en `skip` —típicamente la raíz ops, que no es un
|
|
102
|
+
// servicio del proyecto—. El resultado es una lista, no un veredicto: decidir cuál es el producto y
|
|
103
|
+
// cuál quedó muerto sigue siendo trabajo de una persona o de un cargo.
|
|
104
|
+
function services(root, skip = '') {
|
|
105
|
+
const found = []
|
|
106
|
+
const excluded = skip ? path.resolve(skip) : ''
|
|
107
|
+
const skippable = skipper(root)
|
|
108
|
+
const walk = (dir, depth) => {
|
|
109
|
+
const manifests = manifestsOf(dir)
|
|
110
|
+
if (manifests.length && path.resolve(dir) !== path.resolve(root)) {
|
|
111
|
+
found.push({
|
|
112
|
+
path: path.relative(root, dir).split(path.sep).join('/'),
|
|
113
|
+
runtimes: manifests.map((entry) => entry.runtime),
|
|
114
|
+
manifests: manifests.map((entry) => entry.file),
|
|
115
|
+
commands: commandsOf(dir),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
if (depth >= DEPTH) return
|
|
119
|
+
let entries = []
|
|
120
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isDirectory() || skippable(entry.name)) continue
|
|
123
|
+
const child = path.join(dir, entry.name)
|
|
124
|
+
if (excluded && path.resolve(child) === excluded) continue
|
|
125
|
+
walk(child, depth + 1)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
walk(root, 0)
|
|
129
|
+
return found
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// El primer nivel del workspace también puede ser un solo proyecto sin subcarpetas: se reporta aparte
|
|
133
|
+
// para no confundir «un servicio en la raíz» con «no hay nada».
|
|
134
|
+
function scan(root, skip = '') {
|
|
135
|
+
return {
|
|
136
|
+
root: path.resolve(root),
|
|
137
|
+
rootManifests: manifestsOf(root).map((entry) => entry.file),
|
|
138
|
+
rootCommands: commandsOf(root),
|
|
139
|
+
services: services(root, skip),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { scan, services, IGNORED, DEPTH }
|
package/package.json
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Organización — {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
Este archivo es
|
|
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
|
-
##
|
|
7
|
+
## De qué se trata
|
|
6
8
|
|
|
7
9
|
Por completar.
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## A quién sirve
|
|
10
12
|
|
|
11
|
-
-
|
|
12
|
-
- Quién
|
|
13
|
-
-
|
|
14
|
-
|
|
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
|
|
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
|
|