@ingeniomaps/cauce 0.56.0 → 0.57.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 +28 -0
- package/README.md +3 -1
- package/engine/automation/config.js +15 -19
- package/engine/automation/index.js +9 -5
- package/engine/cli/instance.js +65 -50
- package/engine/cli/ops.js +12 -15
- package/engine/cli/planning.js +7 -1
- package/engine/cli/wiring.js +2 -1
- package/engine/core/onboarding.js +6 -2
- package/engine/planning/contracts.js +14 -0
- package/package.json +1 -1
- package/template/AGENTS.md +12 -15
- package/template/organization/README.md +2 -0
- package/template/organization/workspace.md +34 -0
- package/template/planning/rules/README.md +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,34 @@ 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.57.0] - 2026-09-03
|
|
18
|
+
|
|
19
|
+
### Agregado
|
|
20
|
+
|
|
21
|
+
- **`organization/workspace.md`: dónde va lo que sólo sabe tu proyecto.** El mapa real, las
|
|
22
|
+
integraciones con su entorno concreto y las excepciones de autonomía. Es tuyo y `upgrade` no lo toca.
|
|
23
|
+
Hasta ahora el paso 2 del README te mandaba completar `AGENTS.md`, que es del toolkit y se reemplaza
|
|
24
|
+
entero: no perdías nada —desde 0.55.0 el `upgrade` se detiene y lo nombra— pero te tocaba fusionar a
|
|
25
|
+
mano en cada versión, sobre el único archivo garantizado a entrar en conflicto. Es la separación que
|
|
26
|
+
el toolkit ya usa en `planning/rules/system/` y en `planning/delivery/project.md`.
|
|
27
|
+
|
|
28
|
+
**Si ya llenaste tu `AGENTS.md`**: movés esas secciones a `organization/workspace.md` y listo. El
|
|
29
|
+
`upgrade` te lo dice cuando se detenga, antes de que puedas descartarlas con `--force`.
|
|
30
|
+
|
|
31
|
+
### Corregido
|
|
32
|
+
|
|
33
|
+
- **`check` te dice qué reglas deja de regir un override.** Un archivo propio con el nombre de uno de
|
|
34
|
+
`system/` lo reemplaza entero, no las reglas que mencionás: las que ese archivo definía y el tuyo no
|
|
35
|
+
redefine dejan de existir para tu proyecto. La advertencia nombraba el par de archivos y se leía como
|
|
36
|
+
benigna. El caso caro es una regla que el motor **sigue exigiendo** —R17 lo hace—: quedaba exigida sin
|
|
37
|
+
estar escrita en ningún lado. Ahora dice cuáles: `deja de regir R2, R17…`. Sigue siendo advertencia.
|
|
38
|
+
|
|
39
|
+
- **Instalar un runner ya no hace que `upgrade` te acuse de editar `AGENTS.md`.** En modo `embedded`,
|
|
40
|
+
`automation install` con Codex escribe sus instrucciones dentro de ese archivo, entre marcas. El
|
|
41
|
+
registro anotaba el bloque en un solo lado, así que el `upgrade` siguiente se detenía sobre un archivo
|
|
42
|
+
que nadie había tocado —lo había escrito el comando que el README manda correr justo después—. Lo que
|
|
43
|
+
vos escribas alrededor se sigue detectando igual.
|
|
44
|
+
|
|
17
45
|
## [0.56.0] - 2026-09-03
|
|
18
46
|
|
|
19
47
|
### Corregido
|
package/README.md
CHANGED
|
@@ -224,7 +224,9 @@ equivalentes.
|
|
|
224
224
|
Después de inicializar:
|
|
225
225
|
|
|
226
226
|
1. Edita `ops.config.json`: nombre, modo y raíces de código.
|
|
227
|
-
2. Completa `
|
|
227
|
+
2. Completa `organization/workspace.md`: el mapa real, las integraciones con su entorno y las
|
|
228
|
+
excepciones de autonomía. Es del proyecto y `upgrade` no lo toca; `AGENTS.md`, en cambio, lo
|
|
229
|
+
mantiene Cauce entero.
|
|
228
230
|
3. Completa `organization/company.md` y `organization/product.md` con el contexto estable de la empresa.
|
|
229
231
|
4. Copia `planning/roadmap/epic-000-template.md` a `epic-001-<slug>.md`.
|
|
230
232
|
5. Ejecuta `node tools/ops.js check planning` antes de activar cualquier runner.
|
|
@@ -32,21 +32,17 @@ function mergeConfig(current, incoming) {
|
|
|
32
32
|
return incoming
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Una entrada de hook que puso Cauce se reconoce por el guard al que apunta, y lo nuestro es lo que
|
|
36
|
-
// efectivamente entregamos, no todo lo que vive en nuestra carpeta. Reconocer por directorio
|
|
37
|
-
// desregistraba el guard propio del proyecto en cada reinstalación: `template/AGENTS.md` invita a
|
|
38
|
-
// ponerlo justo ahí y promete que sobrevive a cada actualización, el archivo quedaba en disco, y lo
|
|
39
|
-
// único que se veía era una línea diciendo que se había quitado una entrada obsoleta.
|
|
40
|
-
// Se saca del archivo del usuario antes de fusionar para que el merge deje exactamente las de esta
|
|
41
|
-
// versión, ni una más.
|
|
42
35
|
const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
|
|
43
36
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
37
|
+
// Si una entrada de hook la puso Cauce. Lo nuestro es lo que efectivamente entregamos, no todo lo que
|
|
38
|
+
// vive en nuestra carpeta: `template/AGENTS.md` invita al proyecto a poner su guard justo ahí y le
|
|
39
|
+
// promete que sobrevive a cada actualización, así que reconocer por directorio lo desregistraba en
|
|
40
|
+
// cada reinstalación —el archivo quedaba en disco y el guard dejaba de correr—.
|
|
41
|
+
//
|
|
42
|
+
// Por eso son dos fuentes y no una. `expectedHooks()` es lo que entrega el motor de hoy, y alcanza
|
|
43
|
+
// para una instancia que nunca instaló; `delivered` es lo que ésta registró haber recibido, y es lo
|
|
44
|
+
// único que reconoce un guard que entregamos en una versión y retiramos en la siguiente, cuyo nombre
|
|
45
|
+
// ya no está en la primera. Un guard del proyecto no está en ninguna de las dos, y sobrevive.
|
|
50
46
|
function isDelivered(command, delivered) {
|
|
51
47
|
const hit = String(command).match(HOOK_PATH)
|
|
52
48
|
if (!hit) return false
|
|
@@ -58,6 +54,8 @@ function deliveredHookCommands(commands) {
|
|
|
58
54
|
return [...commands].filter((command) => HOOK_PATH.test(String(command)))
|
|
59
55
|
}
|
|
60
56
|
|
|
57
|
+
// Saca del archivo del usuario lo que pusimos nosotros, para que el merge deje exactamente las
|
|
58
|
+
// entradas de esta versión y ni una más.
|
|
61
59
|
function withoutDeliveredHooks(config, live, delivered = new Set()) {
|
|
62
60
|
const dropped = []
|
|
63
61
|
const walk = (node) => {
|
|
@@ -105,12 +103,10 @@ function reportRemoved(name, dropped, live, output) {
|
|
|
105
103
|
function includesConfig(actual, expected) {
|
|
106
104
|
if (Array.isArray(expected)) {
|
|
107
105
|
return Array.isArray(actual) && expected.every((item) => actual.some((value) => {
|
|
108
|
-
// Un grupo de hooks se compara por su contenido
|
|
109
|
-
//
|
|
110
|
-
// con un hook de más
|
|
111
|
-
//
|
|
112
|
-
// diferencia real— no se miraba nunca. La instalación promete conservar lo que el proyecto ya
|
|
113
|
-
// tenía, así que sumar un hook propio a un grupo nuestro es exactamente lo que permite.
|
|
106
|
+
// Un grupo de hooks se compara por su contenido y no por su forma serializada, porque la
|
|
107
|
+
// instalación promete conservar lo que el proyecto ya tenía: con el ítem entero como unidad, un
|
|
108
|
+
// grupo con un hook propio de más contaba como ausente y `doctor` llamaba divergente a una
|
|
109
|
+
// configuración completa.
|
|
114
110
|
if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
|
|
115
111
|
return value && typeof value === 'object'
|
|
116
112
|
&& value.matcher === item.matcher
|
|
@@ -384,8 +384,6 @@ function install(root, name, output = console, options = {}) {
|
|
|
384
384
|
// guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
|
|
385
385
|
const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
|
|
386
386
|
.map((entry) => JSON.parse(`{${entry}}`).command))
|
|
387
|
-
// Lo que este adaptador anotó haber entregado la última vez. Es lo único que reconoce una entrada
|
|
388
|
-
// nuestra cuyo guard el motor ya no trae: por nombre no se distingue de la que agregó el proyecto.
|
|
389
387
|
const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
|
|
390
388
|
const clean = runner.config.owned
|
|
391
389
|
? { config: {}, dropped: [] }
|
|
@@ -399,9 +397,8 @@ function install(root, name, output = console, options = {}) {
|
|
|
399
397
|
}
|
|
400
398
|
output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
|
|
401
399
|
const deliveredPaths = { ...recorded }
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
// registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
|
|
400
|
+
// Se anota lo que instalamos, no lo que quedó en el archivo: ahí conviven las entradas del proyecto,
|
|
401
|
+
// y registrarlas nos autorizaría a borrar lo ajeno en la instalación siguiente.
|
|
405
402
|
const ourHooks = deliveredHookCommands(live)
|
|
406
403
|
if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
|
|
407
404
|
else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
|
|
@@ -417,6 +414,13 @@ function install(root, name, output = console, options = {}) {
|
|
|
417
414
|
output.log(`✓ ${name}: sus instrucciones quedaron dentro de ${resolved.item.target}`)
|
|
418
415
|
}
|
|
419
416
|
deliveredPaths[deliveryKey(name, resolved.item.target)] = M.digest(resolved.target)
|
|
417
|
+
// Y en la otra sección, porque este archivo está en las dos: `AGENTS.md` es del sistema y es
|
|
418
|
+
// donde este runner deja su bloque. Anotarlo sólo acá dejaba a `localChanges` comparando contra
|
|
419
|
+
// el digest previo al bloque, así que el `upgrade` siguiente se detenía echándole a la empresa
|
|
420
|
+
// una edición que había hecho el comando de al lado. Lo que la empresa escriba alrededor cambia
|
|
421
|
+
// el digest igual, así que se sigue viendo.
|
|
422
|
+
const own = path.relative(root, resolved.target).split(path.sep).join('/')
|
|
423
|
+
if (O.SYSTEM_FILES.includes(own)) M.write(root, M.recordPaths(root, [own], M.read(root)))
|
|
420
424
|
continue
|
|
421
425
|
}
|
|
422
426
|
if (status === 'ajeno' && ownFile) {
|
package/engine/cli/instance.js
CHANGED
|
@@ -30,10 +30,8 @@ function providerNames() {
|
|
|
30
30
|
} catch { return [] }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// Devuelve, por archivo conservado, el digest de lo que
|
|
34
|
-
//
|
|
35
|
-
// el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
|
|
36
|
-
// que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
|
|
33
|
+
// Devuelve, por archivo conservado, el digest de lo que **habría** escrito. Quien registra la entrega
|
|
34
|
+
// necesita esos dos contenidos distintos, y el que quedó en disco no es uno de ellos.
|
|
37
35
|
function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
|
|
38
36
|
F.assertNoSymlinkPath(path.dirname(target), target)
|
|
39
37
|
fs.mkdirSync(target, { recursive: true })
|
|
@@ -115,10 +113,13 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
|
|
|
115
113
|
if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
|
|
116
114
|
}
|
|
117
115
|
deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
116
|
+
// Adoptar Cauce en un repositorio con contenido es `init --force`, y lo que se conserva ahí lo
|
|
117
|
+
// escribió la empresa. Registrarlo hasheando el disco lo declaraba entregado por Cauce con contenido
|
|
118
|
+
// que Cauce nunca entregó, así que `localChanges` no veía ninguna edición y el `upgrade` siguiente lo
|
|
119
|
+
// reemplazaba: entra con el digest del molde, y esa diferencia es lo que detiene al upgrade.
|
|
120
|
+
//
|
|
121
|
+
// Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce rastrea, y un archivo propio
|
|
122
|
+
// fuera de esa frontera no le incumbe.
|
|
122
123
|
for (const [file, hash] of Object.entries(preserved)) {
|
|
123
124
|
const relative = path.relative(root, file).split(path.sep).join('/')
|
|
124
125
|
if (relative in deliveredPaths) deliveredPaths[relative] = hash
|
|
@@ -261,6 +262,51 @@ function renameTeamsToFlows(root) {
|
|
|
261
262
|
return moved
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
// Qué hacer en vez de haber editado, según a quién pertenece cada archivo que se perdería. Vive
|
|
266
|
+
// aparte de `upgrade` porque su reloj es el de la frontera de propiedad y el de la redacción del
|
|
267
|
+
// consejo, no el del procedimiento que lo imprime, y porque así se puede probar sin tocar el disco.
|
|
268
|
+
//
|
|
269
|
+
// Tres clases distintas, y antes eran dos: todo lo que no vivía bajo `system/` recibía el consejo del
|
|
270
|
+
// runtime, así que editar el protocolo respondía con cómo desactivar un guard. Decirle a alguien la
|
|
271
|
+
// salida ajena lo manda a buscar una configuración que no existe.
|
|
272
|
+
function adviceFor(changed) {
|
|
273
|
+
const ruleFiles = changed.filter((file) => file.includes('/system/'))
|
|
274
|
+
const runtime = changed.filter((file) => !file.includes('/system/')
|
|
275
|
+
&& O.RUNTIME_PATHS.some((base) => file.startsWith(`${base}/`)))
|
|
276
|
+
const docs = changed.filter((file) => !ruleFiles.includes(file) && !runtime.includes(file))
|
|
277
|
+
const advice = []
|
|
278
|
+
if (ruleFiles.length) {
|
|
279
|
+
advice.push(
|
|
280
|
+
'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
|
|
281
|
+
+ 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
if (runtime.length) {
|
|
285
|
+
advice.push(
|
|
286
|
+
'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
|
|
287
|
+
+ 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
|
|
288
|
+
+ 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
if (docs.length) {
|
|
292
|
+
advice.push(
|
|
293
|
+
'Esos docs son del toolkit y no llevan una línea de la empresa: se reemplazan enteros en\n'
|
|
294
|
+
+ 'cada actualización para que las mejoras lleguen. Lo que tu proyecto decide distinto va donde sí\n'
|
|
295
|
+
+ 'es suyo —una ADR propia, una regla propia, o `planning/delivery/project.md` para la entrega—.',
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
// `AGENTS.md` se lo gana aparte porque hasta ahora el README mandaba completarlo, así que el consejo
|
|
299
|
+
// genérico de arriba —«no llevan una línea de la empresa»— le miente justo a quien le hizo caso.
|
|
300
|
+
if (docs.includes('AGENTS.md')) {
|
|
301
|
+
advice.push(
|
|
302
|
+
'En `AGENTS.md` en particular: el mapa real, las integraciones y las excepciones de autonomía\n'
|
|
303
|
+
+ 'ahora van en `organization/workspace.md`, que es del proyecto y no se reemplaza. Movelos ahí\n'
|
|
304
|
+
+ 'antes de repetir con --force, o los vas a tener que fusionar de nuevo en cada versión.',
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
return advice.join('\n\n')
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
// Actualiza sólo lo que el toolkit declara suyo. Todo lo demás —planning, organization, reglas
|
|
265
311
|
// propias, agentes editados— queda intacto por construcción, no por comparación.
|
|
266
312
|
function upgrade(dir, cli) {
|
|
@@ -288,19 +334,17 @@ function upgrade(dir, cli) {
|
|
|
288
334
|
const overrides = O.overrides(root)
|
|
289
335
|
|
|
290
336
|
if (dry) {
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
|
|
295
|
-
// la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
|
|
337
|
+
// Antes de mirar versiones, porque son dos preguntas distintas —«¿hay algo más nuevo?» y «¿qué
|
|
338
|
+
// tengo editado que se perdería?»— y la segunda tiene respuesta útil aunque la primera sea que no.
|
|
339
|
+
// Adentro del `if` de abajo, el único modo que no toca nada era el único que no podía avisar.
|
|
296
340
|
for (const file of changed) console.log(` editado localmente: ${file}`)
|
|
297
341
|
if (from === to) {
|
|
298
342
|
// Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
|
|
299
343
|
// importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
|
|
300
344
|
// a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
|
|
301
345
|
console.log(`= ${to}: la instancia está al día con el motor instalado`)
|
|
302
|
-
// Con `--save-exact
|
|
303
|
-
//
|
|
346
|
+
// Con `--save-exact`, sin el cual el caret de npm vuelve falsa la línea de arriba. Por qué, y por
|
|
347
|
+
// qué no alcanza con documentarlo, en `pinEngine`.
|
|
304
348
|
console.log(' para traer una versión más nueva:'
|
|
305
349
|
+ ' npm install --save-exact --save-dev @ingeniomaps/cauce@latest')
|
|
306
350
|
if (changed.length) process.exit(1)
|
|
@@ -331,37 +375,9 @@ function upgrade(dir, cli) {
|
|
|
331
375
|
|
|
332
376
|
if (changed.length && !force) {
|
|
333
377
|
for (const file of changed) console.error(`✗ ${file}`)
|
|
334
|
-
// Tres clases distintas, y antes eran dos: todo lo que no vivía bajo `system/` recibía el consejo
|
|
335
|
-
// del runtime, así que editar el protocolo respondía con cómo desactivar un guard. Cada una tiene
|
|
336
|
-
// su salida y decirle la ajena manda a buscar una configuración que no existe.
|
|
337
|
-
const ruleFiles = changed.filter((file) => file.includes('/system/'))
|
|
338
|
-
const runtime = changed.filter((file) => !file.includes('/system/')
|
|
339
|
-
&& O.RUNTIME_PATHS.some((base) => file.startsWith(`${base}/`)))
|
|
340
|
-
const docs = changed.filter((file) => !ruleFiles.includes(file) && !runtime.includes(file))
|
|
341
|
-
const advice = []
|
|
342
|
-
if (ruleFiles.length) {
|
|
343
|
-
advice.push(
|
|
344
|
-
'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
|
|
345
|
-
+ 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
if (runtime.length) {
|
|
349
|
-
advice.push(
|
|
350
|
-
'El runtime es del toolkit: en vez de editarlo, agregá lo tuyo al lado con otro nombre —un\n'
|
|
351
|
-
+ 'guard propio sobrevive a cada actualización— y registralo en la configuración de tu runner,\n'
|
|
352
|
-
+ 'que sí es del proyecto. Para desactivar un guard alcanza con quitarlo de esa configuración.',
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
if (docs.length) {
|
|
356
|
-
advice.push(
|
|
357
|
-
'Esos docs son del toolkit y no llevan una línea de la empresa: se reemplazan enteros en\n'
|
|
358
|
-
+ 'cada actualización para que las mejoras lleguen. Lo que tu proyecto decide distinto va donde sí\n'
|
|
359
|
-
+ 'es suyo —una ADR propia, una regla propia, o `planning/delivery/project.md` para la entrega—.',
|
|
360
|
-
)
|
|
361
|
-
}
|
|
362
378
|
fail(
|
|
363
379
|
`\n${changed.length} archivo(s) que mantiene Cauce fueron editados y se perderían.\n\n` +
|
|
364
|
-
`${
|
|
380
|
+
`${adviceFor(changed)}\n\nSi el cambio ya no te sirve, repetí con --force para descartarlo.`,
|
|
365
381
|
)
|
|
366
382
|
}
|
|
367
383
|
|
|
@@ -417,8 +433,8 @@ function upgrade(dir, cli) {
|
|
|
417
433
|
// que es la evidencia que el protocolo pide para cualquier cambio.
|
|
418
434
|
for (const file of changed) console.log(`− descartado tu cambio en ${file}`)
|
|
419
435
|
for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
|
|
420
|
-
// Se dice porque
|
|
421
|
-
//
|
|
436
|
+
// Se dice porque explica un diff en un archivo que la empresa versiona, y que si no aparecería sin
|
|
437
|
+
// autor.
|
|
422
438
|
if (pinned === 'ilegible') console.log(' ⚠ package.json no se pudo leer: su versión quedó como estaba')
|
|
423
439
|
else if (pinned) console.log(` package.json: ${pinned} → ${to}, la versión exacta que acabás de aplicar`)
|
|
424
440
|
printChangelog(from, to)
|
|
@@ -426,9 +442,8 @@ function upgrade(dir, cli) {
|
|
|
426
442
|
for (const override of overrides) {
|
|
427
443
|
console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
|
|
428
444
|
}
|
|
429
|
-
// Sólo cuando es cierto
|
|
430
|
-
//
|
|
431
|
-
// todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
|
|
445
|
+
// Sólo cuando es cierto: llegar acá con algo en `changed` es haber descartado contenido de la
|
|
446
|
+
// empresa con --force, que las líneas de arriba enumeran.
|
|
432
447
|
if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
|
|
433
448
|
// No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
|
|
434
449
|
// decide una persona.
|
|
@@ -450,4 +465,4 @@ function upgrade(dir, cli) {
|
|
|
450
465
|
for (const entry of FK.drift(root)) console.log(` ⚠ ${FK.driftLine(entry)}`)
|
|
451
466
|
}
|
|
452
467
|
|
|
453
|
-
module.exports = { copyTemplate, scaffold, providerNames, upgrade, destroy, PROJECT_ROOT }
|
|
468
|
+
module.exports = { copyTemplate, scaffold, providerNames, adviceFor, upgrade, destroy, PROJECT_ROOT }
|
package/engine/cli/ops.js
CHANGED
|
@@ -16,21 +16,20 @@ const BOOT = require('./bootstrap')
|
|
|
16
16
|
// Dónde aterriza una instancia cuando nadie eligió: una carpeta propia junto al código.
|
|
17
17
|
const DEFAULT_TARGET = 'ops'
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// instancia; no hay una segunda adentro.
|
|
23
|
-
function implicitTarget(cwd) {
|
|
24
|
-
return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// La misma heurística, aplicada al destino ya resuelto: una carpeta que ya nombra al toolkit **es** la
|
|
28
|
-
// instancia, y el código de la empresa vive al lado.
|
|
19
|
+
// Una carpeta que ya nombra al toolkit **es** la instancia; no hay una segunda adentro, y el código de
|
|
20
|
+
// la empresa vive al lado. Es la única regla, y de ella salen las dos decisiones que `init` toma sin
|
|
21
|
+
// que nadie las escriba: dónde aterriza y en qué modo.
|
|
29
22
|
function isInstanceDir(dir) {
|
|
30
23
|
const base = path.basename(dir)
|
|
31
24
|
return base === DEFAULT_TARGET || base.endsWith('-ops')
|
|
32
25
|
}
|
|
33
26
|
|
|
27
|
+
// Parada frecuente: el dev ya creó `acme-ops/` y corre `init` adentro. Sin esto la instancia caía en
|
|
28
|
+
// `acme-ops/ops/` y el proyecto quedaba llamándose «acme-ops».
|
|
29
|
+
function implicitTarget(cwd) {
|
|
30
|
+
return isInstanceDir(cwd) ? '.' : DEFAULT_TARGET
|
|
31
|
+
}
|
|
32
|
+
|
|
34
33
|
// El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
|
|
35
34
|
// nombran al toolkit, y quien lee `project` en la configuración espera leer «acme».
|
|
36
35
|
function defaultName(root) {
|
|
@@ -79,11 +78,9 @@ async function init(target, cli) {
|
|
|
79
78
|
// install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
|
|
80
79
|
// único que faltaba era que fuera lo que pasa cuando no se elige nada.
|
|
81
80
|
const root = path.resolve(target || implicitTarget(process.cwd()))
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
|
|
86
|
-
// adentro en vez de al lado, donde el dev abre su herramienta.
|
|
81
|
+
// Del destino resuelto y no de si alguien escribió el argumento: `init` e `init .` apuntan al mismo
|
|
82
|
+
// directorio y elegían modos opuestos, así que escribir el punto daba el layout contrario al de
|
|
83
|
+
// arriba sin que nadie lo pidiera.
|
|
87
84
|
const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
|
|
88
85
|
if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
|
|
89
86
|
const name = cli.value('--name', defaultName(root))
|
package/engine/cli/planning.js
CHANGED
|
@@ -80,7 +80,13 @@ function check(dir, cli) {
|
|
|
80
80
|
// Sobrescribir una entrada de system/ es legítimo y esperado; lo que no puede pasar es que
|
|
81
81
|
// ocurra en silencio, porque esa entrada deja de recibir las mejoras del toolkit.
|
|
82
82
|
for (const override of O.overrides(path.resolve(root, '..'))) {
|
|
83
|
-
|
|
83
|
+
// Y con qué se queda el proyecto: un override sano redefine lo que reemplaza, y el que deja IDs
|
|
84
|
+
// afuera los retira sin decirlo. Nombrarlos es lo único que separa una decisión de un descuido.
|
|
85
|
+
const retired = override.collection === 'planning/rules'
|
|
86
|
+
? PC.retiredByOverride(root, override.project)
|
|
87
|
+
: []
|
|
88
|
+
warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} `
|
|
89
|
+
+ `(override explícito)${retired.length ? `; deja de regir ${retired.join(', ')}` : ''}`)
|
|
84
90
|
}
|
|
85
91
|
// Misma regla para los cargos, que es donde más caro sale: un fork se hace una vez y se olvida.
|
|
86
92
|
const FK = require('../agents/fork')
|
package/engine/cli/wiring.js
CHANGED
|
@@ -179,7 +179,8 @@ function onboard(rootArg, cli, runner = '') {
|
|
|
179
179
|
console.log(runner
|
|
180
180
|
? `\n→ Abrí ${runner} acá y contestale esa pregunta.`
|
|
181
181
|
: '\n→ Contestá esa pregunta cuando corras el arranque.')
|
|
182
|
-
console.log(' Con tus respuestas escribe organization/, el mapa real
|
|
182
|
+
console.log(' Con tus respuestas escribe organization/, el mapa real en'
|
|
183
|
+
+ ' organization/workspace.md y la primera épica.')
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// El registro de proveedores, leído igual por todos los que lo tocan. `list` lo parseaba suelto y un
|
|
@@ -74,7 +74,7 @@ function guide(root, services = []) {
|
|
|
74
74
|
function missingSections(root) {
|
|
75
75
|
const warnings = []
|
|
76
76
|
const template = path.join(PACKAGE_ROOT, 'template', 'organization')
|
|
77
|
-
for (const name of ['company.md', 'product.md', 'domains.md']) {
|
|
77
|
+
for (const name of ['company.md', 'product.md', 'domains.md', 'workspace.md']) {
|
|
78
78
|
const written = path.join(root, 'organization', name)
|
|
79
79
|
if (!fs.existsSync(written) || !fs.existsSync(path.join(template, name))) continue
|
|
80
80
|
const headings = (text) => new Set((text.match(/^##\s+(.+)$/gm) || []).map((line) => line.trim()))
|
|
@@ -99,7 +99,11 @@ function missingSections(root) {
|
|
|
99
99
|
// Sólo cuando la instancia ya tiene contexto escrito: antes del arranque no hay dónde estuvieran.
|
|
100
100
|
function orphanCredentials(root) {
|
|
101
101
|
if (guide(root).fresh) return []
|
|
102
|
-
|
|
102
|
+
// El mapa del proyecto vive en `organization/workspace.md` desde que `AGENTS.md` pasó a ser del
|
|
103
|
+
// toolkit entero; el viejo se sigue mirando porque una instancia anterior lo tiene ahí y una
|
|
104
|
+
// credencial ya declarada no debería volver a reportarse como huérfana por haber mudado el archivo.
|
|
105
|
+
const contracts = [path.join('organization', 'workspace.md'), 'AGENTS.md',
|
|
106
|
+
path.join('planning', 'HUMAN_ACTIONS.md')]
|
|
103
107
|
.map((file) => { try { return fs.readFileSync(path.join(root, file), 'utf8') } catch { return '' } })
|
|
104
108
|
.join('\n')
|
|
105
109
|
if (!contracts) return []
|
|
@@ -172,6 +172,19 @@ function ruleIds(file) {
|
|
|
172
172
|
return [...P.read(file).matchAll(/^##\s+([A-Z]\d+)\s+[—-]/gm)].map((match) => match[1])
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// Qué IDs deja de regir un override por nombre: los que definía el archivo del sistema y el propio no
|
|
176
|
+
// redefine. Reemplazar el archivo entero es la función del override y está documentada; lo que no se
|
|
177
|
+
// veía es la consecuencia, porque la advertencia nombraba el par de archivos y no la diferencia. El
|
|
178
|
+
// caso caro es una regla que el motor sigue exigiendo —R17 lo hace—: queda exigida y sin estar escrita
|
|
179
|
+
// en ningún lado, y quien la vea fallar la va a buscar en `rules/`, donde ya no está.
|
|
180
|
+
function retiredByOverride(dir, name) {
|
|
181
|
+
const rules = path.join(dir, 'rules')
|
|
182
|
+
const system = path.join(rules, 'system', name)
|
|
183
|
+
if (!fs.existsSync(system)) return []
|
|
184
|
+
const redefined = ruleIds(path.join(rules, name))
|
|
185
|
+
return ruleIds(system).filter((id) => !redefined.includes(id))
|
|
186
|
+
}
|
|
187
|
+
|
|
175
188
|
function validateRules(dir) {
|
|
176
189
|
const rules = path.join(dir, 'rules')
|
|
177
190
|
const owner = new Map()
|
|
@@ -418,6 +431,7 @@ module.exports = {
|
|
|
418
431
|
oversizedUnits,
|
|
419
432
|
validateAdr,
|
|
420
433
|
validateRules,
|
|
434
|
+
retiredByOverride,
|
|
421
435
|
validateBacklogStructure,
|
|
422
436
|
validCommitTrace,
|
|
423
437
|
validDecisionTrace,
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -2,24 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Este archivo gobierna el qué y el cuándo. `planning/PROTOCOL.md` gobierna el flujo y
|
|
4
4
|
`planning/rules/` el cómo: sus reglas rigen cada tarea y se leen antes de empezar, no cuando algo sale
|
|
5
|
-
mal.
|
|
6
|
-
|
|
5
|
+
mal. Los tres los mantiene Cauce y valen para cualquier proyecto. Lo que este proyecto tiene de propio
|
|
6
|
+
—su mapa, sus integraciones, hasta dónde llega la autonomía acá— vive en `organization/workspace.md`.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Qué sabe este proyecto y no este archivo
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
El mapa real —qué se construye, qué repos existen, qué está fuera de alcance—, las integraciones con su
|
|
11
|
+
entorno concreto y las excepciones de autonomía viven en **`organization/workspace.md`**, que es del
|
|
12
|
+
proyecto y `upgrade` no toca. Acá no: este archivo lo mantiene Cauce y se reemplaza entero en cada
|
|
13
|
+
actualización, así que lo que escribieras se perdería o te haría fusionar a mano todas las veces.
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- Dónde documenta cada servicio sus comandos de test, lint y build.
|
|
15
|
-
- Qué directorios son legacy o están fuera de alcance.
|
|
16
|
-
|
|
17
|
-
El mapa no debe duplicar documentación técnica: enlaza a la fuente de verdad de cada servicio.
|
|
18
|
-
|
|
19
|
-
## Integraciones y ambientes
|
|
20
|
-
|
|
21
|
-
R12 fija el trato con todo sistema externo: real y de sólo lectura mientras no se lo nombre como
|
|
22
|
-
sandbox. Las excepciones de este proyecto —y sólo ellas— van acá, con el entorno concreto.
|
|
15
|
+
Es la misma separación que `planning/rules/system/` con las reglas propias y que `planning/delivery/`
|
|
16
|
+
con `project.md`: lo que el toolkit mejora, aparte de lo que sólo vos podés escribir.
|
|
23
17
|
|
|
24
18
|
## Cargos disponibles
|
|
25
19
|
|
|
@@ -130,6 +124,9 @@ Registra la acción exacta en `planning/HUMAN_ACTIONS.md` y, si bloquea todo, cr
|
|
|
130
124
|
Nunca amplía el alcance, promueve sus propias ideas, reescribe el proceso durante una tarea, usa
|
|
131
125
|
`git add .`/`git add -A`, hace push/force/amend, ni afirma éxito sin evidencia real.
|
|
132
126
|
|
|
127
|
+
Eso rige sin que nadie escriba nada. Lo que este proyecto amplíe o restrinja va en
|
|
128
|
+
`organization/workspace.md`, con su razón; los cuatro límites del párrafo anterior no se amplían ahí.
|
|
129
|
+
|
|
133
130
|
## Definición de terminado
|
|
134
131
|
|
|
135
132
|
1. La aceptación se observa y queda cubierta por pruebas cuando existe superficie testeable.
|
|
@@ -7,6 +7,8 @@ Con molde, y `check` avisa si a alguno le faltan secciones del original:
|
|
|
7
7
|
- `company.md`: misión, modelo de negocio, objetivos, estructura y derechos de decisión.
|
|
8
8
|
- `product.md`: problema, usuarios, propuesta de valor y límites.
|
|
9
9
|
- `domains.md`: lenguaje ubicuo, dominios y dueños.
|
|
10
|
+
- `workspace.md`: el mapa real, las integraciones con su entorno y las excepciones de autonomía.
|
|
11
|
+
Es lo que `AGENTS.md` no puede llevar: ese archivo lo mantiene Cauce y se reemplaza al actualizar.
|
|
10
12
|
|
|
11
13
|
Recomendados, sin molde: escribilos con la forma que le sirva a este proyecto.
|
|
12
14
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Este workspace
|
|
2
|
+
|
|
3
|
+
Lo que sólo sabe este proyecto: qué hay, contra qué se conecta y hasta dónde puede decidir solo un
|
|
4
|
+
runner. `AGENTS.md` gobierna el qué y el cuándo para cualquier proyecto; esto es lo que cambia entre
|
|
5
|
+
uno y otro, y por eso vive acá, donde Cauce no escribe: `upgrade` no lo toca nunca.
|
|
6
|
+
|
|
7
|
+
## Mapa real
|
|
8
|
+
|
|
9
|
+
Completar antes de la primera tarea:
|
|
10
|
+
|
|
11
|
+
- Qué producto se construye.
|
|
12
|
+
- Qué repos o servicios existen y dónde viven.
|
|
13
|
+
- Dónde documenta cada servicio sus comandos de test, lint y build.
|
|
14
|
+
- Qué directorios son legacy o están fuera de alcance.
|
|
15
|
+
|
|
16
|
+
El mapa no debe duplicar documentación técnica: enlaza a la fuente de verdad de cada servicio.
|
|
17
|
+
|
|
18
|
+
## Integraciones y ambientes
|
|
19
|
+
|
|
20
|
+
R12 fija el trato con todo sistema externo: real y de producción mientras no se lo nombre como sandbox.
|
|
21
|
+
Las excepciones de este proyecto —y sólo ellas— van acá, nombrando el entorno concreto.
|
|
22
|
+
|
|
23
|
+
Las credenciales que cada integración necesita se nombran acá o en `planning/HUMAN_ACTIONS.md`, con
|
|
24
|
+
quién las carga y dónde: `check` avisa cuando el proyecto declara una variable que no aparece en
|
|
25
|
+
ninguno de los dos, porque una credencial sin dueño no rompe nada hasta el día del despliegue.
|
|
26
|
+
|
|
27
|
+
## Excepciones de autonomía
|
|
28
|
+
|
|
29
|
+
Los límites que rigen sin escribir nada están en `AGENTS.md`, y son los mismos para todo proyecto. Acá
|
|
30
|
+
va lo que este proyecto amplía o restringe, con su razón: un servicio donde el runner no puede tocar
|
|
31
|
+
migraciones, un repo donde sí puede commitear directo, un entorno de pruebas que es seguro escribir.
|
|
32
|
+
|
|
33
|
+
Lo que no se puede ampliar acá: promover trabajo propio, prometer fechas, inventar evidencia o exceder
|
|
34
|
+
la autoridad de un cargo. Eso no depende del proyecto.
|
|
@@ -19,4 +19,10 @@ fuera de él para que no existan dos reglas con el mismo nombre.
|
|
|
19
19
|
Para cambiar una regla del sistema, escribí la tuya **con el mismo nombre de archivo**: ahí redefinir
|
|
20
20
|
sus números es la función, la del proyecto manda, y `check` reporta el override en vez de fallar.
|
|
21
21
|
|
|
22
|
+
Lo que reemplaza es **el archivo entero, no las reglas que mencionás**: las que ese archivo del sistema
|
|
23
|
+
definía y el tuyo no redefine dejan de regir para tu proyecto. Alguna la sigue exigiendo el motor —R17
|
|
24
|
+
lo hace—, y ahí queda exigida sin estar escrita en ningún lado. Por eso `check` te dice cuáles son
|
|
25
|
+
—«deja de regir R2, R17…»— y por eso conviene, antes de sobrescribir un archivo, mirar si lo tuyo era
|
|
26
|
+
una regla nueva: si lo era, va acá al lado como `P1..Pn` y no se lleva nada puesto.
|
|
27
|
+
|
|
22
28
|
Las convenciones específicas de lenguaje viven junto al servicio que usa ese lenguaje.
|