@ingeniomaps/cauce 0.54.0 → 0.55.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,49 @@ 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.55.0] - 2026-09-02
18
+
19
+ ### Corregido
20
+
21
+ - **`upgrade` ya no pisa lo que `init --force` conservó.** Adoptar Cauce en un repositorio que ya tiene
22
+ contenido es `init --force` y después `upgrade`. El primero conserva tus archivos y lo cumple; el
23
+ segundo los reemplazaba sin avisar e imprimía que no había tocado nada propio. El registro se grababa
24
+ hasheando el disco, así que un archivo conservado quedaba anotado como entregado por Cauce con **tu**
25
+ contenido, y la comparación no veía ninguna diferencia. Ahora se anota el digest de lo que el molde
26
+ habría escrito: tu archivo se ve como lo que es —una edición local— y `upgrade` se detiene
27
+ nombrándolo. **Qué cambia para vos**: el primer `upgrade` después de una adopción con `--force` se
28
+ detiene listando esos archivos. Resolvelos a mano, o repetí con `--force` para descartarlos, que
29
+ ahora además los enumera en la salida.
30
+ - **Tu guard propio sobrevive a `automation install`.** `AGENTS.md` te dice que lo pongas en
31
+ `automatization/hooks/guard-<nombre>.sh` y promete que sobrevive a cada actualización. Sobrevivía al
32
+ `upgrade` y no a la reinstalación del runner: lo nuestro se reconocía por la carpeta, así que
33
+ cualquier `guard-*.sh` de ahí se desregistraba antes de fusionar. El archivo quedaba en disco, el
34
+ guard dejaba de correr y lo único que se veía era una línea diciendo que se había quitado una entrada
35
+ obsoleta. Ahora se reconoce por lo que efectivamente entregamos: los guards que el motor trae, más
36
+ los que esta instancia anotó haber recibido —el manifiesto gana una entrada por runner con el wiring
37
+ que dejó puesto, y por eso una entrada nuestra se sigue retirando el día que el motor deje de traer
38
+ ese guard—. **Si moviste tu guard fuera de esa carpeta para sortearlo, ya podés devolverlo.**
39
+ - **`automation doctor` deja de llamar divergente a una configuración completa.** Un hook propio sumado
40
+ *dentro* de un grupo que escribió el toolkit dejaba a `doctor` en rojo aunque estuvieran todas las
41
+ entradas esperadas: comparaba el grupo entero serializado, así que uno con un hook de más contaba
42
+ como ausente. Ahora compara por `matcher` y contenido. Lo que falta se sigue reportando igual.
43
+ - **`upgrade --check` avisa de lo que tenés editado aunque la versión coincida.** Salía por un camino
44
+ corto que informaba «al día» sin llegar a listarlo, que es el estado normal entre actualizaciones
45
+ —`init` fija la versión exacta—: el único modo que no toca nada era también el único que no podía
46
+ anticipar el conflicto. **Qué cambia para vos**: `--check` ahora sale 1 cuando hay archivos del
47
+ sistema editados, un caso donde antes salía 0. Si lo tenés en un script, revisá esa condición.
48
+
49
+ ### Cambiado
50
+
51
+ - **`init` e `init .` eligen el mismo modo en el mismo directorio.** El default salía de si escribiste
52
+ el argumento, no de a dónde apunta: en `acme-ops/`, `init` daba `sidecar` e `init .` daba `embedded`.
53
+ No es cosmético — en `embedded` la raíz del workspace pasa a ser la carpeta ops misma, así que
54
+ `guard-workspace-boundary` deja de reconocer los repos hermanos y el wiring del runner se escribe
55
+ adentro en vez de al lado, donde abrís tu herramienta. Ahora lo decide el destino resuelto, con la
56
+ misma regla que ya elegía dónde aterrizar: una carpeta llamada `ops` o terminada en `-ops` **es** la
57
+ instancia. **Qué cambia para vos**: `init .` en una carpeta así ahora da `sidecar`, donde antes daba
58
+ `embedded`. El caso embebido sigue disponible pidiéndolo: `init . --mode embedded`.
59
+
17
60
  ## [0.54.0] - 2026-09-02
18
61
 
19
62
  ### Agregado
@@ -8,7 +8,7 @@
8
8
  const fs = require('node:fs')
9
9
  const path = require('node:path')
10
10
  const F = require('../core/files')
11
- const { supersededGuards } = require('./hooks')
11
+ const { supersededGuards, expectedHooks } = require('./hooks')
12
12
 
13
13
  function mergeConfig(current, incoming) {
14
14
  if (Array.isArray(incoming)) {
@@ -32,19 +32,40 @@ 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: `automatization/hooks/`
36
- // es nuestro y ninguna empresa escribe ahí. Se saca del archivo del usuario antes de fusionar para que
37
- // el merge deje exactamente las de esta versión, ni una más.
38
- const DELIVERED = /automatization\/hooks\/guard-[a-z-]+\.sh/
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
+ const HOOK_PATH = /automatization\/hooks\/([a-z0-9-]+\.sh)(?:\s|$)/
39
43
 
40
- function withoutDeliveredHooks(config, live) {
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.
50
+ function isDelivered(command, delivered) {
51
+ const hit = String(command).match(HOOK_PATH)
52
+ if (!hit) return false
53
+ return expectedHooks().includes(hit[1]) || delivered.has(String(command))
54
+ }
55
+
56
+ // Lo que de un conjunto de comandos es wiring de guards nuestro, para anotarlo como entregado.
57
+ function deliveredHookCommands(commands) {
58
+ return [...commands].filter((command) => HOOK_PATH.test(String(command)))
59
+ }
60
+
61
+ function withoutDeliveredHooks(config, live, delivered = new Set()) {
41
62
  const dropped = []
42
63
  const walk = (node) => {
43
64
  if (Array.isArray(node)) {
44
65
  return node
45
66
  .filter((item) => {
46
67
  const command = item && typeof item === 'object' ? String(item.command || '') : ''
47
- if (!DELIVERED.test(command)) return true
68
+ if (!isDelivered(command, delivered)) return true
48
69
  // Sólo se anuncia lo que ya no vuelve: una entrada que el merge repone quedó igual, y decir
49
70
  // que se quitó y se puso la misma línea es ruido que esconde el caso que sí importa.
50
71
  if (!live.has(command)) dropped.push(command)
@@ -83,9 +104,20 @@ function reportRemoved(name, dropped, live, output) {
83
104
 
84
105
  function includesConfig(actual, expected) {
85
106
  if (Array.isArray(expected)) {
86
- return Array.isArray(actual) && expected.every((item) => {
87
- return actual.some((value) => JSON.stringify(value) === JSON.stringify(item))
88
- })
107
+ 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.
114
+ if (item && typeof item === 'object' && Array.isArray(item.hooks)) {
115
+ return value && typeof value === 'object'
116
+ && value.matcher === item.matcher
117
+ && includesConfig(value.hooks, item.hooks)
118
+ }
119
+ return JSON.stringify(value) === JSON.stringify(item)
120
+ }))
89
121
  }
90
122
  if (expected && typeof expected === 'object') {
91
123
  return actual && typeof actual === 'object' && Object.entries(expected).every(([key, value]) => {
@@ -170,6 +202,6 @@ function blockUpToDate(file, name, content) {
170
202
 
171
203
  module.exports = {
172
204
  blockStart,
173
- mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
205
+ mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
174
206
  unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
175
207
  }
@@ -17,7 +17,7 @@ const {
17
17
  legacyGuardWiring, staleHooks, listHooks,
18
18
  } = require('./hooks')
19
19
  const {
20
- blockStart, mergeConfig, withoutDeliveredHooks, reportRemoved, includesConfig, hasHooks,
20
+ blockStart, mergeConfig, withoutDeliveredHooks, deliveredHookCommands, reportRemoved, includesConfig, hasHooks,
21
21
  unmergeConfig, isSharedFile, withoutBlock, mergeInstruction, blockUpToDate,
22
22
  } = require('./config')
23
23
 
@@ -226,6 +226,11 @@ function deliveryKey(name, target) {
226
226
  return `${name}/${target.split(path.sep).join('/')}`
227
227
  }
228
228
 
229
+ // La sección `runners` del manifiesto anota archivo → digest; ésta es la única entrada que guarda una
230
+ // lista, porque lo que registra no es un archivo sino las entradas que dejamos dentro del archivo de
231
+ // configuración del usuario. No colisiona con ningún `target`: ninguno se llama así.
232
+ const HOOKS_KEY = 'config.hooks'
233
+
229
234
  // En qué estado quedó un archivo que este adaptador entregó alguna vez.
230
235
  //
231
236
  // nuevo no existe todavía
@@ -277,6 +282,7 @@ function uninstall(root, name, output = console) {
277
282
 
278
283
  const items = [...(runner.instructions || []), ...(runner.artifacts || [])]
279
284
  const deliveredPaths = { ...recorded }
285
+ delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
280
286
  for (const item of items) {
281
287
  const resolved = { item, ...resolveItem(paths, root, name, item) }
282
288
  const key = deliveryKey(name, item.target)
@@ -378,7 +384,12 @@ function install(root, name, output = console, options = {}) {
378
384
  // guard que el runner intenta ejecutar y falla. Se quitan las nuestras y las vuelve a poner el merge.
379
385
  const live = new Set((JSON.stringify(incoming).match(/"command":"[^"]*"/g) || [])
380
386
  .map((entry) => JSON.parse(`{${entry}}`).command))
381
- const clean = runner.config.owned ? { config: {}, dropped: [] } : withoutDeliveredHooks(current, live)
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
+ const previous = new Set(recorded[deliveryKey(name, HOOKS_KEY)] || [])
390
+ const clean = runner.config.owned
391
+ ? { config: {}, dropped: [] }
392
+ : withoutDeliveredHooks(current, live, previous)
382
393
  reportRemoved(name, clean.dropped, live, output)
383
394
  F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
384
395
  // Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
@@ -388,6 +399,12 @@ function install(root, name, output = console, options = {}) {
388
399
  }
389
400
  output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
390
401
  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.
405
+ const ourHooks = deliveredHookCommands(live)
406
+ if (ourHooks.length) deliveredPaths[deliveryKey(name, HOOKS_KEY)] = ourHooks
407
+ else delete deliveredPaths[deliveryKey(name, HOOKS_KEY)]
391
408
  for (const resolved of resolvedItems) {
392
409
  const status = state.get(resolved)
393
410
  const ownFile = runner.instructions.includes(resolved.item)
@@ -29,9 +29,14 @@ function providerNames() {
29
29
  } catch { return [] }
30
30
  }
31
31
 
32
+ // Devuelve, por archivo conservado, el digest de lo que el molde **habría** escrito. Adoptar Cauce en
33
+ // un repositorio con contenido es `init --force`, y lo que se conserva ahí lo escribió la empresa: si
34
+ // el manifiesto lo registra hasheando el disco queda declarado como entregado por Cauce con contenido
35
+ // que Cauce nunca entregó, y el `upgrade` siguiente no ve ninguna edición local y lo reemplaza.
32
36
  function copyTemplate(source, target, replacements, force, skip = [], quiet = false) {
33
37
  F.assertNoSymlinkPath(path.dirname(target), target)
34
38
  fs.mkdirSync(target, { recursive: true })
39
+ const preserved = {}
35
40
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
36
41
  if (skip.includes(entry.name)) continue
37
42
  const from = path.join(source, entry.name)
@@ -39,11 +44,14 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
39
44
  // 11.16.0: lo deja afuera en la raíz y en subdirectorios—, así que viaja sin punto y se restituye
40
45
  // acá. Sin esto el archivo existe en el repo del toolkit y desaparece para todo consumidor real.
41
46
  const to = path.join(target, entry.name === 'gitignore' ? '.gitignore' : entry.name)
42
- if (entry.isDirectory()) copyTemplate(from, to, replacements, force, skip, quiet)
47
+ if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
43
48
  else {
44
49
  if (fs.existsSync(to)) {
45
50
  if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
46
51
  if (!quiet) console.log(`= conservado ${to}`)
52
+ let would = fs.readFileSync(from, 'utf8')
53
+ for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
54
+ preserved[to] = M.digestText(would)
47
55
  continue
48
56
  }
49
57
  let content = fs.readFileSync(from, 'utf8')
@@ -53,22 +61,29 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
53
61
  if (!quiet) console.log(`+ ${to}`)
54
62
  }
55
63
  }
64
+ return preserved
56
65
  }
57
66
 
67
+ // Devuelve lo conservado igual que `copyTemplate`, y por la misma razón: acá el runtime no lleva
68
+ // reemplazos, así que lo que habríamos escrito es el archivo del paquete tal cual.
58
69
  function copyRuntime(source, target, preserve = false, boundary = target, skip = []) {
59
70
  F.assertNoSymlinkPath(boundary, target)
60
71
  fs.mkdirSync(target, { recursive: true })
72
+ const preserved = {}
61
73
  for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
62
74
  if (skip.includes(entry.name)) continue
63
75
  const from = path.join(source, entry.name)
64
76
  const to = path.join(target, entry.name)
65
- if (entry.isDirectory()) copyRuntime(from, to, preserve, boundary, skip)
66
- else if (preserve && fs.existsSync(to)) console.log(`= conservado ${to}`)
67
- else {
77
+ if (entry.isDirectory()) Object.assign(preserved, copyRuntime(from, to, preserve, boundary, skip))
78
+ else if (preserve && fs.existsSync(to)) {
79
+ console.log(`= conservado ${to}`)
80
+ preserved[to] = M.digest(from)
81
+ } else {
68
82
  F.assertNoSymlinkPath(boundary, to)
69
83
  fs.copyFileSync(from, to)
70
84
  }
71
85
  }
86
+ return preserved
72
87
  }
73
88
 
74
89
  // Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
@@ -116,7 +131,7 @@ function undeclareEngine(manifest) {
116
131
  // el banco de evaluación lo llama directo: crear una instancia programáticamente no puede depender de
117
132
  // cómo venga escrita la línea de comandos.
118
133
  function scaffold(root, { name, mode, force = false, quiet = false }) {
119
- copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
134
+ const preserved = copyTemplate(path.join(PROJECT_ROOT, 'template'), root, {
120
135
  '{{PROJECT_NAME}}': name,
121
136
  '{{MODE}}': mode,
122
137
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
@@ -124,12 +139,12 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
124
139
  // No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
125
140
  // el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
126
141
  // `.github/workflows/` vacío en cada instancia.
127
- copyRuntime(
142
+ Object.assign(preserved, copyRuntime(
128
143
  path.join(PROJECT_ROOT, 'automatization', 'hooks'),
129
144
  path.join(root, 'automatization', 'hooks'),
130
145
  force,
131
146
  root,
132
- )
147
+ ))
133
148
  const version = require(path.join(PROJECT_ROOT, 'package.json')).version
134
149
  // El motor llega como dependencia para que el lockfile fije su versión. El repo ops es un sidecar:
135
150
  // declarar npm acá no convierte en Node al servicio de Go de al lado.
@@ -140,6 +155,14 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
140
155
  if (fs.existsSync(dir)) deliveredPaths = M.record(root, relative, O.treeFiles(dir), deliveredPaths)
141
156
  }
142
157
  deliveredPaths = M.recordPaths(root, O.SYSTEM_FILES, deliveredPaths)
158
+ // Lo conservado entra con el digest del molde, no con el del disco: la diferencia entre los dos es
159
+ // justamente lo que `localChanges` tiene que ver para que `upgrade` se detenga antes de pisar el
160
+ // archivo de la empresa. Sólo se pisan claves que ya están: el manifiesto declara lo que Cauce
161
+ // rastrea, y un archivo propio fuera de esa frontera no le incumbe.
162
+ for (const [file, hash] of Object.entries(preserved)) {
163
+ const relative = path.relative(root, file).split(path.sep).join('/')
164
+ if (relative in deliveredPaths) deliveredPaths[relative] = hash
165
+ }
143
166
  M.write(root, deliveredPaths)
144
167
  // La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
145
168
  const configFile = path.join(root, 'ops.config.json')
@@ -305,12 +328,20 @@ function upgrade(dir, cli) {
305
328
  const overrides = O.overrides(root)
306
329
 
307
330
  if (dry) {
331
+ // Lo editado localmente se informa siempre, antes de mirar versiones: son dos preguntas distintas
332
+ // —«¿hay algo más nuevo?» y «¿qué tengo editado que se perdería?»— y la segunda tiene respuesta
333
+ // útil aunque la primera sea que no. Sin esto, el único modo que no toca nada era también el único
334
+ // que no podía avisar del conflicto, justo en el estado normal entre actualizaciones: `init` fija
335
+ // la versión exacta, así que instancia y motor coinciden casi todo el tiempo.
336
+ for (const file of changed) console.log(` editado localmente: ${file}`)
308
337
  if (from === to) {
309
338
  // Contra el motor instalado, no contra lo publicado: la comparación es local y sin red. Decirlo
310
339
  // importa porque `init` fija la versión exacta, así que el motor no se mueve solo y esta línea,
311
340
  // a secas, se leía como «no hay nada nuevo» durante todas las versiones siguientes.
312
341
  console.log(`= ${to}: la instancia está al día con el motor instalado`)
313
- return console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
342
+ console.log(' para traer una versión más nueva: npm install --save-dev @ingeniomaps/cauce@latest')
343
+ if (changed.length) process.exit(1)
344
+ return
314
345
  }
315
346
  // Hacia atrás también es legítimo —una versión rompió algo y se vuelve—, pero anunciarlo como «hay
316
347
  // una versión más nueva» era mentir con el número a la vista. Y lo que corresponde imprimir es lo
@@ -322,7 +353,6 @@ function upgrade(dir, cli) {
322
353
  console.log(`⚠ hay una versión más nueva: ${to} (la instancia tiene ${from || 'una previa'})`)
323
354
  printChangelog(from, to)
324
355
  }
325
- for (const file of changed) console.log(` editado localmente: ${file}`)
326
356
  process.exit(1)
327
357
  }
328
358
 
@@ -427,7 +457,10 @@ function upgrade(dir, cli) {
427
457
  for (const override of overrides) {
428
458
  console.log(`= conservado ${override.collection}/${override.project}: sobrescribe ${override.system}`)
429
459
  }
430
- console.log(' planning, organization y todo lo propio quedaron intactos')
460
+ // Sólo cuando es cierto. Llegar acá con algo en `changed` significa que se corrió con --force y se
461
+ // descartó contenido de la empresa: las líneas de arriba lo enumeran, y afirmar a continuación que
462
+ // todo lo propio quedó intacto contradice a la única señal que recibe quien corrió el comando.
463
+ if (!changed.length) console.log(' planning, organization y todo lo propio quedaron intactos')
431
464
  // No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
432
465
  // decide una persona.
433
466
  if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
package/engine/cli/ops.js CHANGED
@@ -21,8 +21,14 @@ const DEFAULT_TARGET = 'ops'
21
21
  // de otra— y el proyecto quedaba llamándose «acme-ops». La carpeta que ya nombra al toolkit es la
22
22
  // instancia; no hay una segunda adentro.
23
23
  function implicitTarget(cwd) {
24
- const base = path.basename(cwd)
25
- return base === DEFAULT_TARGET || base.endsWith('-ops') ? '.' : DEFAULT_TARGET
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.
29
+ function isInstanceDir(dir) {
30
+ const base = path.basename(dir)
31
+ return base === DEFAULT_TARGET || base.endsWith('-ops')
26
32
  }
27
33
 
28
34
  // El nombre sale de la carpeta del proyecto, no de la que aloja la instancia: `ops/` y `acme-ops/`
@@ -73,7 +79,12 @@ async function init(target, cli) {
73
79
  // install` ya asume —el wiring del runner va al padre, donde se abre la herramienta—, así que lo
74
80
  // único que faltaba era que fuera lo que pasa cuando no se elige nada.
75
81
  const root = path.resolve(target || implicitTarget(process.cwd()))
76
- const mode = cli.value('--mode', target ? 'embedded' : 'sidecar')
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.
87
+ const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
77
88
  if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
78
89
  const name = cli.value('--name', defaultName(root))
79
90
  const force = cli.has('--force')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",