@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 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 `AGENTS.md`: límites de autonomía e integraciones reales.
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
- // Dos fuentes, y la segunda es la que cierra el borde. `expectedHooks()` dice qué entrega el motor de
45
- // hoy y alcanza para una instancia que nunca instaló; `delivered` es lo que esta instancia registró
46
- // haber recibido la última vez, y es lo único que reconoce un guard que entregamos en una versión y
47
- // retiramos en la siguiente: su nombre ya no está en la lista de hoy, así que sin el registro quedaría
48
- // vivo en la configuración llamando a un guard que Cauce ya no mantiene. Un guard propio del proyecto
49
- // no está en ninguna de las dos, que es exactamente por lo que sobrevive.
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, no por su forma serializada. La comprobación es
109
- // «contiene», que es lo correcto, pero con el ítem entero como unidad un grupo `{matcher, hooks}`
110
- // con un hook de más dejaba de ser el mismo objeto y contaba como ausente: `doctor` llamaba
111
- // divergente a una configuración que tenía todo lo esperado, y el anidamiento —donde vive la
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
- // Qué wiring de guards dejamos puesto, para poder retirarlo el día que el motor deje de traerlo. Se
403
- // anota lo que instalamos y no lo que quedó en el archivo: ahí conviven las entradas del proyecto, y
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) {
@@ -30,10 +30,8 @@ function providerNames() {
30
30
  } catch { return [] }
31
31
  }
32
32
 
33
- // Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
34
- // un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
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
- // Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
119
- // justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
120
- // archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
121
- // rastrea, y un archivo propio fuera de esa frontera no le incumbe.
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
- // Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
292
- // —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
293
- // útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
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` porque npm guarda con caret por defecto, y el caret es lo que volvería
303
- // falsa la línea de arriba: dentro de 0.x alcanza a los patches, y Cauce publica patches.
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
- `${advice.join('\n\n')}\n\nSi el cambio ya no te sirve, repetí con --force para descartarlo.`,
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 cambia un archivo que la empresa versiona, y porque explica un diff que si no
421
- // aparecería sin autor. Ilegible se avisa y no se toca: el manifiesto es del repo anfitrión.
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. Llegar acá con algo en `changed` significa que se corrió con --force y se
430
- // descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
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
- // Dónde va la instancia cuando nadie eligió destino. Parada frecuente: el dev ya creó `acme-ops/` y
20
- // corre `init` adentro. Sin esto la instancia caía en `acme-ops/ops/` —una carpeta del toolkit dentro
21
- // de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
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
- // El modo lo decide a dónde apunta el destino, no si alguien escribió el argumento. `init` e `init .`
83
- // resuelven el mismo directorio y elegían modos opuestos: escribir el punto —la forma más natural de
84
- // decir «acá»— daba embedded, y ahí la raíz del workspace pasa a ser la carpeta ops misma, así que
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))
@@ -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
- warnings.push(`${override.collection}/${override.project} sobrescribe ${override.system} (override explícito)`)
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')
@@ -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 de AGENTS.md y la primera épica.')
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
- const contracts = ['AGENTS.md', path.join('planning', 'HUMAN_ACTIONS.md')]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",
@@ -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. Ahí está la conducta que no depende de este proyecto —trato con sistemas externos, qué se debe
6
- entregar aun al negarse—; acá, lo que sí depende.
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
- ## Mapa real
8
+ ## Qué sabe este proyecto y no este archivo
9
9
 
10
- Completar antes de la primera tarea:
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
- - Qué producto se construye.
13
- - Qué repos o servicios existen y dónde viven.
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.