@ingeniomaps/cauce 0.27.0 → 0.28.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 +35 -0
- package/README.md +16 -3
- package/automatization/runners/antigravity/manifest.json +4 -0
- package/automatization/runners/antigravity/skills/onboard/SKILL.md +30 -0
- package/automatization/runners/claude/CLAUDE.md +3 -2
- package/automatization/runners/claude/manifest.json +4 -0
- package/automatization/runners/codex/AGENTS.md +14 -0
- package/automatization/runners/gemini/GEMINI.md +14 -0
- package/automatization/workflows/onboard.js +256 -0
- package/engine/cli/ops.js +22 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,41 @@ 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.28.0] - 2026-08-18
|
|
18
|
+
|
|
19
|
+
### Añadido
|
|
20
|
+
|
|
21
|
+
- **`/onboard`: el recorrido que llena una instancia recién creada.** `init` la deja funcionando y sin
|
|
22
|
+
enterar de nada —`organization/` es el molde y el roadmap está vacío—, y llenarlo exige leer el
|
|
23
|
+
repositorio y decidir qué es cada cosa, que es justo lo que un CLI determinista no puede hacer. El
|
|
24
|
+
recorrido inventaría los subproyectos con manifiesto propio, **corre** los comandos de test, lint y
|
|
25
|
+
build que cada uno declara, y escribe `organization/`, la sección «Mapa real» de `AGENTS.md` y las
|
|
26
|
+
raíces reales en `ops.config.json`.
|
|
27
|
+
|
|
28
|
+
Corre los comandos en vez de copiarlos del README porque un mapa copiado envejece sin avisar y el
|
|
29
|
+
primer Verify de una tarea real es donde aparece: cada comando queda como verificado, falla o ausente.
|
|
30
|
+
Lo que deduce va marcado «(supuesto)» y lo que nada sostiene queda «Por definir». No lee `.env` ni
|
|
31
|
+
ninguna credencial —de `.env.example` toma sólo los nombres de variable—, y las credenciales, los MCP
|
|
32
|
+
y el permiso de push salen como filas de `HUMAN_ACTIONS.md` con la acción concreta que las desbloquea,
|
|
33
|
+
sin proponer ningún valor. Cierra escribiendo la épica 001 —que una tarea pueda atravesar el ciclo
|
|
34
|
+
entero— y no la promueve.
|
|
35
|
+
|
|
36
|
+
Si la instancia ya tiene contexto escrito, para y pide `force`: reescribir un borrador que alguien
|
|
37
|
+
corrigió no deja rastro de lo que se perdió. Y un workspace todavía sin código no es un error: escribe
|
|
38
|
+
lo que el contexto permita y traer los repos pasa a ser la primera historia.
|
|
39
|
+
|
|
40
|
+
Claude y Antigravity lo reciben como recorrido ejecutable; Codex y Gemini lo operan desde la sección
|
|
41
|
+
«El arranque» que `automation install` deja en sus instrucciones. Si ya tenés una instancia, el
|
|
42
|
+
workflow llega con `automation install` —no con `upgrade`—: los workflows viven en el runner.
|
|
43
|
+
|
|
44
|
+
### Corregido
|
|
45
|
+
|
|
46
|
+
- **`init` dentro de una carpeta que ya nombra al toolkit deja de anidar.** Correrlo parado en
|
|
47
|
+
`acme-ops/` creaba `acme-ops/ops/` —una raíz ops dentro de otra— y llamaba «acme-ops» al proyecto.
|
|
48
|
+
Ahora la instancia es esa carpeta y el proyecto se llama «acme». Además un `.git` solo dejó de contar
|
|
49
|
+
como contenido, así que `mkdir acme-ops && git init && cauce init` no pide `--force` para no pisar
|
|
50
|
+
nada.
|
|
51
|
+
|
|
17
52
|
## [0.27.0] - 2026-08-18
|
|
18
53
|
|
|
19
54
|
### Añadido
|
package/README.md
CHANGED
|
@@ -96,9 +96,22 @@ falta igual —el motor, los guards y los workflows son JavaScript—.
|
|
|
96
96
|
|
|
97
97
|
### El primer ciclo
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
`init` deja la instancia funcionando, no enterada: `organization/` llega como molde y el roadmap está
|
|
100
|
+
vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que es lo que un CLI determinista
|
|
101
|
+
no puede hacer, así que ese recorrido vive en el runner:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
/onboard inventaría los servicios, corre sus comandos de test, lint y build, y escribe
|
|
105
|
+
organization/, el «Mapa real» de AGENTS.md y las raíces de ops.config.json.
|
|
106
|
+
Lo deducido queda marcado como supuesto; credenciales, MCP y el permiso de push
|
|
107
|
+
van a HUMAN_ACTIONS.md. Cierra con la épica 001, sin promoverla.
|
|
108
|
+
/team evalúa si una intención posterior es viable y propone su épica.
|
|
109
|
+
/autobuild ejecuta una tarea ya promovida, fase por fase.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Claude y Antigravity lo traen como recorrido ejecutable; Codex y Gemini lo operan siguiendo las
|
|
113
|
+
instrucciones que `automation install` les deja. Sin runner, la [tabla de comandos](#comandos) y
|
|
114
|
+
[FLOW.md](template/planning/FLOW.md) hacen el mismo camino a mano.
|
|
102
115
|
|
|
103
116
|
Dentro del proyecto el CLI se invoca con `node tools/ops.js` —o `npx cauce`, que la dependencia deja
|
|
104
117
|
disponible—; desde este repositorio, con `node engine/cli/ops.js`. En la tabla de abajo `ops` representa
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onboard
|
|
3
|
+
description: Escanea el repositorio y deja escrito el contexto de la empresa y la primera épica.
|
|
4
|
+
---
|
|
5
|
+
|
|
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ó.
|
|
10
|
+
|
|
11
|
+
Inventariá los subproyectos con manifiesto propio, sin entrar en la raíz ops ni en `node_modules`. De cada
|
|
12
|
+
uno tomá su ruta, para qué sirve y los comandos de test, lint y build que él mismo declara. **Corré esos
|
|
13
|
+
comandos**: un mapa copiado del README envejece sin avisar y el primer Verify de una tarea real descubre
|
|
14
|
+
que el comando no existe. Nunca corras migraciones, deploys ni publicaciones, aunque un script se llame
|
|
15
|
+
así.
|
|
16
|
+
|
|
17
|
+
Con eso escribí `{{OPS_DIR}}organization/company.md` y `product.md`, la sección «Mapa real» de
|
|
18
|
+
`{{OPS_DIR}}AGENTS.md` con el resultado que obtuviste por comando, y las raíces reales en
|
|
19
|
+
`workspaceRoots` de `{{OPS_DIR}}ops.config.json`, que es lo que un guard usa para bloquear una escritura
|
|
20
|
+
fuera de lugar. Lo deducido va marcado `(supuesto)` y lo que nada sostiene queda «Por definir»: no
|
|
21
|
+
inventes clientes, ingresos ni objetivos.
|
|
22
|
+
|
|
23
|
+
Credenciales, MCP y el permiso de push no te corresponden. Cada uno va como fila en
|
|
24
|
+
`{{OPS_DIR}}planning/HUMAN_ACTIONS.md` con la acción concreta que lo desbloquea y sin proponer ningún
|
|
25
|
+
valor; las preguntas abiertas, a la sección Ideas de `{{OPS_DIR}}planning/INBOX.md`.
|
|
26
|
+
|
|
27
|
+
Cerrá escribiendo `epic-001` en `{{OPS_DIR}}planning/roadmap/`: su resultado es que una tarea pueda
|
|
28
|
+
atravesar el ciclo entero, y sus criterios salen de lo que hoy falta —contexto sin supuestos, cada
|
|
29
|
+
comando en verde, el guard de límites probado en las dos direcciones, una tarea piloto en DONE—. Validá
|
|
30
|
+
con `node {{OPS_DIR}}tools/ops.js check planning`. **Nunca promuevas al BACKLOG**: esa firma es humana.
|
|
@@ -7,8 +7,9 @@
|
|
|
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.
|
|
11
|
-
y
|
|
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
|
|
12
|
+
intención es viable y propone una épica, y `/autobuild` ejecuta trabajo ya promovido; `/integration-sync` e
|
|
12
13
|
`/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
|
|
13
14
|
|
|
14
15
|
Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/WIP.md`. Si el protocolo y
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"source": "../../workflows/team.js",
|
|
30
30
|
"target": ".claude/workflows/team.js"
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
"source": "../../workflows/onboard.js",
|
|
34
|
+
"target": ".claude/workflows/onboard.js"
|
|
35
|
+
},
|
|
32
36
|
{
|
|
33
37
|
"source": "../../workflows/agent-eval.js",
|
|
34
38
|
"target": ".claude/workflows/agent-eval.js"
|
|
@@ -31,6 +31,20 @@ node {{OPS_DIR}}tools/ops.js agents list
|
|
|
31
31
|
Leé el `SKILL.md` del cargo que corresponda antes de actuar en su terreno, y respetá sus límites. Lo que
|
|
32
32
|
ese cargo debe saber de esta empresa está en `{{OPS_DIR}}organization/roles/<slug>.md`.
|
|
33
33
|
|
|
34
|
+
## El arranque
|
|
35
|
+
|
|
36
|
+
En una instancia recién creada nadie le explicó todavía al toolkit qué es este proyecto:
|
|
37
|
+
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
|
|
38
|
+
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
39
|
+
|
|
40
|
+
Inventariá los subproyectos con manifiesto propio y **corré** los comandos de test, lint y build que cada
|
|
41
|
+
uno declara —un mapa copiado del README envejece sin avisar—. Con eso escribí `{{OPS_DIR}}organization/`,
|
|
42
|
+
la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con el resultado por comando, y las raíces reales en
|
|
43
|
+
`workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
44
|
+
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
45
|
+
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
46
|
+
lo que falta. Nunca la promuevas.
|
|
47
|
+
|
|
34
48
|
## Los equipos
|
|
35
49
|
|
|
36
50
|
Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
|
|
@@ -24,6 +24,20 @@ node {{OPS_DIR}}tools/ops.js agents list
|
|
|
24
24
|
node {{OPS_DIR}}tools/ops.js team list
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## El arranque
|
|
28
|
+
|
|
29
|
+
En una instancia recién creada nadie le explicó todavía al toolkit qué es este proyecto:
|
|
30
|
+
`{{OPS_DIR}}organization/` llega como molde y el roadmap está vacío. El primer recorrido lo llena, y una vez: reescribir un contexto
|
|
31
|
+
que alguien ya corrigió no deja rastro de lo que se perdió.
|
|
32
|
+
|
|
33
|
+
Inventariá los subproyectos con manifiesto propio y **corré** los comandos de test, lint y build que cada
|
|
34
|
+
uno declara —un mapa copiado del README envejece sin avisar—. Con eso escribí `{{OPS_DIR}}organization/`,
|
|
35
|
+
la sección «Mapa real» de `{{OPS_DIR}}AGENTS.md` con el resultado por comando, y las raíces reales en
|
|
36
|
+
`workspaceRoots`. Lo deducido va marcado `(supuesto)`. Credenciales, MCP y el permiso de push van como
|
|
37
|
+
filas en `{{OPS_DIR}}planning/HUMAN_ACTIONS.md`, sin proponer valores. Cerrá con `epic-001` en
|
|
38
|
+
`{{OPS_DIR}}planning/roadmap/`: que una tarea pueda atravesar el ciclo entero, con criterios que salen de
|
|
39
|
+
lo que falta. Nunca la promuevas.
|
|
40
|
+
|
|
27
41
|
Nunca omitas aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones
|
|
28
42
|
trabajo desde INBOX.
|
|
29
43
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Arranque de una instancia recién creada: convierte un repositorio que nadie le explicó al toolkit en
|
|
2
|
+
// contexto escrito —`organization/`, el mapa real de `AGENTS.md`, las raíces de código— y en la primera
|
|
3
|
+
// épica. Es el paso que `init` no puede dar: instalar es determinista, y leer un repositorio para decidir
|
|
4
|
+
// qué es el producto, qué carpeta es legacy y cuál es el comando que de verdad lo valida, no lo es.
|
|
5
|
+
//
|
|
6
|
+
// Escribe borradores y no decide por nadie: cada dato deducido queda marcado como supuesto, las
|
|
7
|
+
// credenciales y los sistemas externos van a HUMAN_ACTIONS —R12 se los prohíbe a un runner— y la épica
|
|
8
|
+
// queda en roadmap/ sin promover, que sigue siendo la firma humana.
|
|
9
|
+
export const meta = {
|
|
10
|
+
name: 'onboard',
|
|
11
|
+
description: 'Escanea el repositorio y deja escrito el contexto de la empresa y la primera épica',
|
|
12
|
+
whenToUse: 'Primera corrida después de "cauce init", cuando organization/ y el roadmap están vacíos.',
|
|
13
|
+
phases: [
|
|
14
|
+
{ title: 'Scan', detail: 'Servicios, manifiestos y comandos declarados' },
|
|
15
|
+
{ title: 'Verify', detail: 'Los comandos se corren: el mapa no se copia del README' },
|
|
16
|
+
{ title: 'Draft', detail: 'organization/, mapa real, raíces y acciones humanas' },
|
|
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' },
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// El prefijo lo completa `automation install`. Igual que en los demás workflows: el runtime no expone
|
|
23
|
+
// `process`, así que la ruta de la raíz ops viaja escrita, relativa a donde se abre la herramienta.
|
|
24
|
+
const ROOT = '{{OPS_DIR}}'.replace(/\/+$/, '') || '.'
|
|
25
|
+
const P = `${ROOT}/planning`
|
|
26
|
+
const ORG = `${ROOT}/organization`
|
|
27
|
+
const HUMAN = `${P}/HUMAN_ACTIONS.md`
|
|
28
|
+
const INBOX = `${P}/INBOX.md`
|
|
29
|
+
const ROADMAP = `${P}/roadmap`
|
|
30
|
+
|
|
31
|
+
// Lo que la persona ya sabe y no hace falta deducir: `/onboard vendemos ruteo a PYMEs de logística`.
|
|
32
|
+
// Entra como contexto, no como verdad: lo que diga acá se escribe como hecho, y lo deducido no.
|
|
33
|
+
const input = typeof args === 'string' ? { context: args } : (args || {})
|
|
34
|
+
const CONTEXT = String(input.context || '').trim()
|
|
35
|
+
const FORCE = Boolean(input.force)
|
|
36
|
+
|
|
37
|
+
const BASE = `Nunca inventes clientes, métricas, ingresos, plazos ni responsables. Distinguí siempre lo ` +
|
|
38
|
+
`que verificaste corriendo algo, lo que leíste en un archivo del repositorio y lo que estás ` +
|
|
39
|
+
`suponiendo: lo tercero va marcado como "(supuesto)" en el texto que escribas. No leas archivos de ` +
|
|
40
|
+
`credenciales —.env, *.pem, claves— ni copies su contenido a ningún lado; .env.example sí, y sólo los ` +
|
|
41
|
+
`nombres de las variables. No escribas en ningún sistema externo, no conectes nada y no promuevas ` +
|
|
42
|
+
`trabajo al BACKLOG.`
|
|
43
|
+
|
|
44
|
+
function finish(result) {
|
|
45
|
+
log(`Fin: ${JSON.stringify(result)}`)
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stop = (reason, detail = '') => {
|
|
50
|
+
log(`Checkpoint: ${reason}${detail ? ` — ${detail}` : ''}`)
|
|
51
|
+
return finish({ stopped: true, reason, detail })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STATE = {
|
|
55
|
+
type: 'object', additionalProperties: false, required: ['fresh'],
|
|
56
|
+
properties: {
|
|
57
|
+
fresh: { type: 'boolean' },
|
|
58
|
+
reason: { type: 'string' },
|
|
59
|
+
mode: { type: 'string' },
|
|
60
|
+
workspaceRoots: { type: 'array', items: { type: 'string' } },
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const INVENTORY = {
|
|
65
|
+
type: 'object', additionalProperties: false, required: ['services'],
|
|
66
|
+
properties: {
|
|
67
|
+
services: { type: 'array', items: { type: 'object', additionalProperties: false,
|
|
68
|
+
required: ['path', 'runtime'], properties: {
|
|
69
|
+
path: { type: 'string' }, runtime: { type: 'string' }, purpose: { type: 'string' },
|
|
70
|
+
test: { type: 'string' }, lint: { type: 'string' }, build: { type: 'string' },
|
|
71
|
+
source: { type: 'string' },
|
|
72
|
+
} } },
|
|
73
|
+
legacy: { type: 'array', items: { type: 'string' } },
|
|
74
|
+
ci: { type: 'array', items: { type: 'string' } },
|
|
75
|
+
externals: { type: 'array', items: { type: 'string' } },
|
|
76
|
+
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
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const WRITTEN = {
|
|
97
|
+
type: 'object', additionalProperties: false, required: ['files', 'assumptions'],
|
|
98
|
+
properties: {
|
|
99
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
100
|
+
assumptions: { type: 'array', items: { type: 'string' } },
|
|
101
|
+
humanActions: { type: 'array', items: { type: 'string' } },
|
|
102
|
+
openQuestions: { type: 'array', items: { type: 'string' } },
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
phase('Scan')
|
|
107
|
+
|
|
108
|
+
// Arrancar dos veces sobre la misma instancia reescribiría contexto que una persona ya corrigió, y el
|
|
109
|
+
// borrador se lee igual que el original: nada delataría la pérdida. Se comprueba antes de leer nada más.
|
|
110
|
+
const state = await agent(
|
|
111
|
+
`${BASE}\n\nFrom the workspace root, report whether this instance is still untouched. Read ` +
|
|
112
|
+
`${ROOT}/ops.config.json for its mode and workspaceRoots, ${ORG}/company.md and ${ORG}/product.md, and ` +
|
|
113
|
+
`list ${ROADMAP}. Set fresh=true only if the organization files still carry the mold's "Por completar" ` +
|
|
114
|
+
`or "Por definir" placeholders and the roadmap holds nothing but its template and README. Otherwise ` +
|
|
115
|
+
`set fresh=false and say in reason what is already written.`,
|
|
116
|
+
{ schema: STATE, label: 'estado-inicial' },
|
|
117
|
+
)
|
|
118
|
+
if (!state) return stop('estado-desconocido', 'no se pudo leer el estado de la instancia')
|
|
119
|
+
if (!state.fresh && !FORCE) {
|
|
120
|
+
return stop('ya-arrancado', `${state.reason || 'la instancia ya tiene contexto escrito'}. ` +
|
|
121
|
+
`Pasá force:true si querés reescribir los borradores.`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inventory = await agent(
|
|
125
|
+
`${BASE}\n\nInventariá el repositorio desde la raíz del workspace, sin entrar en ${ROOT}/ ni en ` +
|
|
126
|
+
`node_modules, vendor, dist o build. Para cada subproyecto con manifiesto propio —package.json, ` +
|
|
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')
|
|
154
|
+
|
|
155
|
+
// Un mapa copiado del README envejece sin avisar y el primer Verify de una tarea real descubre que el
|
|
156
|
+
// comando no existe. Correrlos acá es barato y es lo que separa "documentado" de "verificado".
|
|
157
|
+
const checks = (await parallel(services.map((service) => () => agent(
|
|
158
|
+
`${BASE}\n\nFrom the workspace root, check the commands declared by the service at "${service.path}": ` +
|
|
159
|
+
`test=${service.test || '(ninguno)'}, lint=${service.lint || '(ninguno)'}, ` +
|
|
160
|
+
`build=${service.build || '(ninguno)'}. Run each one that exists, from that directory, and report what ` +
|
|
161
|
+
`happened: "verificado" when it finished green, "falla" with the first meaningful error line when it ` +
|
|
162
|
+
`did not, "ausente" when the service declares none. Never run migrations, deploys, publishes, or ` +
|
|
163
|
+
`anything that writes outside this repository, even if a script has that name: report it as ausente ` +
|
|
164
|
+
`and say why.`,
|
|
165
|
+
{ schema: CHECKED, label: `verify:${service.path}`, phase: 'Verify' },
|
|
166
|
+
)))).filter(Boolean)
|
|
167
|
+
|
|
168
|
+
const green = checks.flatMap((entry) => entry.results.filter((result) => result.status === 'verificado')).length
|
|
169
|
+
const broken = checks.flatMap((entry) => entry.results.filter((result) => result.status === 'falla'))
|
|
170
|
+
log(`${green} comando(s) verificados, ${broken.length} con fallo. Un fallo no detiene el arranque: se escribe.`)
|
|
171
|
+
|
|
172
|
+
phase('Draft')
|
|
173
|
+
|
|
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
|
+
const drafted = await agent(
|
|
179
|
+
`${BASE}\n\n${EVIDENCE}\n\nEscribí los borradores de contexto de esta instancia. Reemplazá el molde, no ` +
|
|
180
|
+
`lo comentes:\n` +
|
|
181
|
+
`1. ${ORG}/company.md y ${ORG}/product.md: lo que el repositorio y el contexto aportado permiten ` +
|
|
182
|
+
`afirmar. Lo deducido va marcado "(supuesto)"; lo que nada sostiene queda como "Por definir" y su ` +
|
|
183
|
+
`pregunta va a openQuestions. No inventes clientes, ingresos ni objetivos.\n` +
|
|
184
|
+
`2. La sección "## Mapa real" de ${ROOT}/AGENTS.md: una entrada por servicio con su ruta, para qué ` +
|
|
185
|
+
`sirve y sus comandos, cada uno con el resultado que obtuviste —verificado, falla o ausente—. Enlazá ` +
|
|
186
|
+
`la fuente en vez de duplicar documentación técnica, y nombrá lo legacy y lo fuera de alcance.\n` +
|
|
187
|
+
`3. ${ROOT}/ops.config.json: dejá en workspaceRoots las raíces de código reales. Es lo que un guard usa ` +
|
|
188
|
+
`para bloquear una escritura fuera de lugar, así que una raíz de más lo apaga.\n` +
|
|
189
|
+
`Devolvé en files cada archivo que tocaste y en assumptions cada supuesto que dejaste marcado.`,
|
|
190
|
+
{ schema: WRITTEN, label: 'contexto' },
|
|
191
|
+
)
|
|
192
|
+
if (!drafted) return stop('draft-unavailable', 'los borradores no devolvieron resultado')
|
|
193
|
+
|
|
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
|
+
phase('Epic')
|
|
208
|
+
|
|
209
|
+
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` +
|
|
212
|
+
`Escribí en ${ROADMAP} la épica epic-001-<slug>.md siguiendo el contrato de ${P}/PROTOCOL.md: ` +
|
|
213
|
+
`frontmatter epic/title/status/service con status open, criterios **CN** observables, "## Contexto ` +
|
|
214
|
+
`relevante" con rutas verificadas, e historias con (→ CN) y (service: ruta), cada una de menos de ` +
|
|
215
|
+
`cuatro horas.\n\n` +
|
|
216
|
+
`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
|
+
`organization/ no tenga supuestos sin confirmar, que cada servicio del mapa tenga su comando ` +
|
|
219
|
+
`corriendo en verde, que las raíces declaradas hagan que el guard de límites bloquee una escritura ` +
|
|
220
|
+
`afuera y deje pasar una adentro, y que una tarea piloto real llegue a DONE con evidencia. Si algún ` +
|
|
221
|
+
`comando falló, arreglarlo es una historia, no un criterio aparte. En "## Riesgos y decisiones ` +
|
|
222
|
+
`humanas" citá las filas que dejaste en HUMAN_ACTIONS. No toques BACKLOG.md.`,
|
|
223
|
+
{ schema: { type: 'object', additionalProperties: false, required: ['file', 'criteria', 'stories'],
|
|
224
|
+
properties: {
|
|
225
|
+
file: { type: 'string' }, title: { type: 'string' },
|
|
226
|
+
criteria: { type: 'array', items: { type: 'string' } },
|
|
227
|
+
stories: { type: 'array', items: { type: 'string' } },
|
|
228
|
+
} }, label: 'epica-001' },
|
|
229
|
+
)
|
|
230
|
+
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')
|
|
242
|
+
|
|
243
|
+
const supuestos = (drafted.assumptions || []).length
|
|
244
|
+
const acciones = ((pending || {}).humanActions || []).length
|
|
245
|
+
log(`Contexto escrito con ${supuestos} supuesto(s) por confirmar y ${acciones} acción(es) humana(s) en ${HUMAN}.`)
|
|
246
|
+
log(`Épica en ${epic.file}, sin promover: revisala, promoví una historia a un hito del BACKLOG y corré /autobuild.`)
|
|
247
|
+
|
|
248
|
+
return finish({
|
|
249
|
+
services: services.length,
|
|
250
|
+
verified: green,
|
|
251
|
+
broken: broken.length,
|
|
252
|
+
assumptions: supuestos,
|
|
253
|
+
humanActions: acciones,
|
|
254
|
+
epic: epic.file,
|
|
255
|
+
promoted: false,
|
|
256
|
+
})
|
package/engine/cli/ops.js
CHANGED
|
@@ -226,6 +226,15 @@ function evaluationBench(root, agent, caso, force) {
|
|
|
226
226
|
return dir
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Dónde va la instancia cuando nadie eligió destino. Parada frecuente: el dev ya creó `acme-ops/` y
|
|
230
|
+
// corre `init` adentro. Sin esto la instancia caía en `acme-ops/ops/` —una carpeta del toolkit dentro
|
|
231
|
+
// de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
|
|
232
|
+
// instancia; no hay una segunda adentro.
|
|
233
|
+
function implicitTarget(cwd) {
|
|
234
|
+
const base = path.basename(cwd)
|
|
235
|
+
return base === DEFAULT_TARGET || base.endsWith('-ops') ? '.' : DEFAULT_TARGET
|
|
236
|
+
}
|
|
237
|
+
|
|
229
238
|
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
230
239
|
// nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
|
|
231
240
|
function defaultName(root) {
|
|
@@ -268,12 +277,15 @@ async function init(target, cli) {
|
|
|
268
277
|
// nivel deja de distinguir qué es suyo y qué llegó del toolkit. Es el layout que `automation
|
|
269
278
|
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
270
279
|
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
271
|
-
const root = path.resolve(target ||
|
|
280
|
+
const root = path.resolve(target || implicitTarget(process.cwd()))
|
|
272
281
|
const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
|
|
273
282
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
274
283
|
const name = cli.value('--name', defaultName(root))
|
|
275
284
|
const force = cli.has('--force')
|
|
276
|
-
|
|
285
|
+
// `.git` no cuenta como contenido: es lo único que hay en la carpeta que alguien acaba de crear y
|
|
286
|
+
// versionar para la instancia, y el toolkit no escribe nada adentro. Sin esta excepción el camino
|
|
287
|
+
// más natural —`mkdir acme-ops && git init && cauce init`— pedía `--force` para no pisar nada.
|
|
288
|
+
const existing = (fs.existsSync(root) ? fs.readdirSync(root) : []).filter((entry) => entry !== '.git')
|
|
277
289
|
if (existing.length && !force) {
|
|
278
290
|
fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
|
|
279
291
|
}
|
|
@@ -306,6 +318,14 @@ async function init(target, cli) {
|
|
|
306
318
|
|
|
307
319
|
if (resultado.instalado) {
|
|
308
320
|
check(path.join(root, 'planning'), SIN_BANDERAS)
|
|
321
|
+
// Una instancia recién instalada funciona y no sabe nada de este proyecto: `organization/` es el
|
|
322
|
+
// molde y el roadmap está vacío. Llenarlo exige leer el repositorio y decidir qué es cada cosa, que
|
|
323
|
+
// es justo lo que un CLI determinista no puede hacer; el recorrido vive en el runner, así que lo
|
|
324
|
+
// único útil acá es decir cuál es y con qué se abre.
|
|
325
|
+
if (resultado.runner !== BOOT.SIN_RUNNER) {
|
|
326
|
+
console.log(` siguiente: abrí ${resultado.runner} en este directorio y corré /onboard`)
|
|
327
|
+
console.log(' escanea el repositorio y deja escrito el contexto de la empresa y la primera épica')
|
|
328
|
+
}
|
|
309
329
|
console.log(` listo: el ciclo empieza en ${path.join(relative || '.', 'planning', 'FLOW.md')}`)
|
|
310
330
|
}
|
|
311
331
|
for (const paso of initSteps(enter, resultado)) console.log(paso)
|