@ingeniomaps/cauce 0.5.0 → 0.5.2

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
@@ -8,6 +8,38 @@ esa operación sea confiable en vez de sólo cómoda: acá se lee qué cambió a
8
8
  un cambio en el protocolo, en las reglas del sistema o en un guard es visible para el usuario y sube
9
9
  minor aunque no toque una sola línea de código.
10
10
 
11
+ ## [0.5.2] - 2026-08-15
12
+
13
+ ### Corregido
14
+
15
+ - **`upgrade` registraba mal lo que entregaba, y eso trababa el siguiente.** El registro se anotaba
16
+ ruta por ruta releyendo el manifiesto del disco en cada una, así que la última anulaba a todas las
17
+ anteriores: tras un `upgrade` casi todos los digests quedaban viejos y la actualización siguiente
18
+ los leía como ediciones de la empresa. Un callejón sin salida sin que nadie hubiera tocado nada.
19
+
20
+ Una instancia que ya quedó con digests viejos se destraba con `cauce upgrade --force`: el contenido
21
+ en disco y el del paquete son el mismo, así que no se descarta nada real.
22
+
23
+ ## [0.5.1] - 2026-08-15
24
+
25
+ ### Agregado
26
+
27
+ - **Codex recibe su `AGENTS.md`.** Era el único runner sin archivo de instrucciones: se llevaba los
28
+ guards y nada más, así que podía ser detenido pero no sabía que existía un protocolo, un catálogo de
29
+ cargos ni equipos. No existe un `CODEX.md`: `AGENTS.md` es el nombre que Codex lee, compartido entre
30
+ herramientas, y por eso en modo embedded no se instala —el de la empresa ya está ahí y manda—.
31
+
32
+ ### Corregido
33
+
34
+ - **Las rutas de los adaptadores se resuelven contra la carpeta donde se abre la herramienta.** Al
35
+ mover la instalación a la raíz de la compañía quedaron apuntando al lugar equivocado: `@AGENTS.md`
36
+ y `@planning/PROTOCOL.md` no resolvían, y los workflows buscaban `planning/` donde no estaba. Ahora
37
+ cada fuente marca el lugar con `{{OPS_DIR}}` y `install` lo completa; `doctor` compara contra lo
38
+ mismo que `install` escribe.
39
+ - `tools/ops.js` exporta la raíz ops, que ya calculaba y no usaba. Invocado desde la carpeta de la
40
+ compañía —`node <empresa>-ops/tools/ops.js team list`—, `agents list` y `team list` resolvían contra
41
+ el cwd y devolvían **vacío en vez de fallar**. `team` además no aceptaba una raíz de ningún modo.
42
+
11
43
  ## [0.5.0] - 2026-08-15
12
44
 
13
45
  ### Cambiado
@@ -1,6 +1,6 @@
1
1
  # Cauce
2
2
 
3
- Lee y cumple `AGENTS.md` y `planning/PROTOCOL.md` antes de ejecutar trabajo. `planning/WIP.md` es el mutex de
4
- ejecución y `planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
3
+ Lee y cumple `AGENTS.md` y `{{OPS_DIR}}planning/PROTOCOL.md` antes de ejecutar trabajo. `{{OPS_DIR}}planning/WIP.md` es el mutex de
4
+ ejecución y `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva. No promociones ideas desde INBOX, no
5
5
  inventes aprobaciones o credenciales y no hagas push ni deploy. Cierra cada tarea con verificación real y
6
6
  evidencia en DONE.
@@ -1,11 +1,11 @@
1
1
  # Cauce para Claude Code
2
2
 
3
- @AGENTS.md
4
- @planning/PROTOCOL.md
3
+ @{{OPS_DIR}}AGENTS.md
4
+ @{{OPS_DIR}}planning/PROTOCOL.md
5
5
 
6
6
  Los hooks de `.claude/settings.json` son obligatorios. Usa `/team` para evaluar si una intención es viable
7
7
  y proponer una épica, y `/autobuild` para ejecutar trabajo ya promovido; `/integration-sync` e
8
8
  `/integration-promote` gestionan staging local sin escritura remota. Ninguno promueve al BACKLOG.
9
9
 
10
- Antes de iniciar, respeta `planning/AWAITING_REVIEW.md` y el mutex de `planning/WIP.md`. Si el protocolo y
11
- un workflow difieren, manda el protocolo y la diferencia se registra en `planning/INBOX.md`.
10
+ Antes de iniciar, respeta `{{OPS_DIR}}planning/AWAITING_REVIEW.md` y el mutex de `{{OPS_DIR}}planning/WIP.md`. Si el protocolo y
11
+ un workflow difieren, manda el protocolo y la diferencia se registra en `{{OPS_DIR}}planning/INBOX.md`.
@@ -4,20 +4,20 @@
4
4
  {
5
5
  "matcher": "Bash",
6
6
  "hooks": [
7
- { "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-shell.sh" }
7
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-shell.sh" }
8
8
  ]
9
9
  },
10
10
  {
11
11
  "matcher": "Edit|Write",
12
12
  "hooks": [
13
- { "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-files.sh" }
13
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh" }
14
14
  ]
15
15
  }
16
16
  ],
17
17
  "Stop": [
18
18
  {
19
19
  "hooks": [
20
- { "type": "command", "command": "$CLAUDE_PROJECT_DIR/automatization/hooks/guard-planning-drift.sh" }
20
+ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-planning-drift.sh" }
21
21
  ]
22
22
  }
23
23
  ]
@@ -0,0 +1,47 @@
1
+ # Cauce para Codex
2
+
3
+ `{{OPS_DIR}}AGENTS.md` tiene las reglas del sistema y `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de
4
+ verdad del proceso. Leelos antes de trabajar: acá sólo está lo específico de este runner.
5
+
6
+ > Codex lee el `AGENTS.md` de la raíz, que es un nombre compartido entre herramientas. Cuando el repo
7
+ > ops **es** la raíz, este archivo no se instala: el `AGENTS.md` de la empresa ya está ahí y manda.
8
+
9
+ Los hooks de `.codex/hooks/hooks.json` son obligatorios y bloquean por su cuenta. Codex no ejecuta los
10
+ workflows JS de Claude —son referencia, no un runtime compatible—, así que el recorrido se hace fase por
11
+ fase siguiendo el protocolo.
12
+
13
+ `{{OPS_DIR}}planning/WIP.md` es el mutex: una sola tarea activa. `{{OPS_DIR}}planning/AWAITING_REVIEW.md`
14
+ bloquea una corrida nueva hasta que un humano revise. Nada se promueve desde `INBOX.md` sin aprobación.
15
+
16
+ Antes de cerrar, corré `node {{OPS_DIR}}tools/ops.js check {{OPS_DIR}}planning`.
17
+
18
+ ## Los cargos
19
+
20
+ El catálogo está en `{{OPS_DIR}}node_modules/@ingeniomaps/cauce/agents/` —o en
21
+ `{{OPS_DIR}}.ops/agents/` si este repo no usa npm— y los propios de la empresa en
22
+ `{{OPS_DIR}}agents/`, que mandan sobre los del sistema con el mismo nombre.
23
+
24
+ Cada cargo tiene un `SKILL.md` con su contrato: cuándo actúa, qué decide, qué no le corresponde y cuál es
25
+ su entrega mínima. Para ver la lista con una línea por cargo:
26
+
27
+ ```bash
28
+ node {{OPS_DIR}}tools/ops.js agents list
29
+ ```
30
+
31
+ Leé el `SKILL.md` del cargo que corresponda antes de actuar en su terreno, y respetá sus límites. Lo que
32
+ ese cargo debe saber de esta empresa está en `{{OPS_DIR}}organization/roles/<slug>.md`.
33
+
34
+ ## Los equipos
35
+
36
+ Un equipo es una secuencia de cargos con etapas y exit gates, para evaluar una intención antes de que
37
+ exista una épica. Están en `{{OPS_DIR}}node_modules/@ingeniomaps/cauce/teams/system/` y los propios en
38
+ `{{OPS_DIR}}teams/`.
39
+
40
+ ```bash
41
+ node {{OPS_DIR}}tools/ops.js team list
42
+ node {{OPS_DIR}}tools/ops.js team show <slug>
43
+ ```
44
+
45
+ Ningún equipo promueve trabajo al BACKLOG: escribe la épica o el informe y para.
46
+
47
+ Nunca omitas aprobaciones, inventes credenciales, escribas en un sistema remoto, hagas push o deploy.
@@ -4,20 +4,20 @@
4
4
  {
5
5
  "matcher": "shell|unified_exec",
6
6
  "hooks": [
7
- { "type": "command", "command": "automatization/hooks/guard-shell.sh" }
7
+ { "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-shell.sh" }
8
8
  ]
9
9
  },
10
10
  {
11
11
  "matcher": "apply_patch|files",
12
12
  "hooks": [
13
- { "type": "command", "command": "automatization/hooks/guard-files.sh" }
13
+ { "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-files.sh" }
14
14
  ]
15
15
  }
16
16
  ],
17
17
  "SessionEnd": [
18
18
  {
19
19
  "hooks": [
20
- { "type": "command", "command": "automatization/hooks/guard-planning-drift.sh" }
20
+ { "type": "command", "command": "{{OPS_DIR}}automatization/hooks/guard-planning-drift.sh" }
21
21
  ]
22
22
  }
23
23
  ]
@@ -6,7 +6,12 @@
6
6
  "source": "hooks.json",
7
7
  "target": ".codex/hooks/hooks.json"
8
8
  },
9
- "instructions": [],
9
+ "instructions": [
10
+ {
11
+ "source": "AGENTS.md",
12
+ "target": "AGENTS.md"
13
+ }
14
+ ],
10
15
  "artifacts": [],
11
16
  "capabilities": {
12
17
  "nativeHooks": true,
@@ -1,12 +1,12 @@
1
1
  # Cauce para Gemini CLI
2
2
 
3
- @AGENTS.md
4
- @planning/PROTOCOL.md
3
+ @{{OPS_DIR}}AGENTS.md
4
+ @{{OPS_DIR}}planning/PROTOCOL.md
5
5
 
6
- `planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/ops:autobuild` fase por fase; los workflows JS de
7
- Claude son referencia, no un runtime compatible. `planning/WIP.md` es el mutex y
8
- `planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
6
+ `{{OPS_DIR}}planning/PROTOCOL.md` es la fuente de verdad. Ejecuta `/ops:autobuild` fase por fase; los workflows JS de
7
+ Claude son referencia, no un runtime compatible. `{{OPS_DIR}}planning/WIP.md` es el mutex y
8
+ `{{OPS_DIR}}planning/AWAITING_REVIEW.md` bloquea una corrida nueva.
9
9
 
10
10
  Gemini no tiene guards nativos configurados por este toolkit. Antes de cada commit ejecuta los wrappers de
11
- `automatization/hooks/` como prechecks y al cerrar ejecuta `node tools/ops.js check planning`. Nunca omitas
11
+ `{{OPS_DIR}}automatization/hooks/` como prechecks y al cerrar ejecuta `node {{OPS_DIR}}tools/ops.js check planning`. Nunca omitas
12
12
  aprobaciones, inventes credenciales, escribas remoto, hagas push/deploy o promociones trabajo desde INBOX.
@@ -10,7 +10,10 @@ export const meta = {
10
10
  ].map((title) => ({ title, detail: `Fase ${title} del protocolo agnóstico` })),
11
11
  }
12
12
 
13
- const ROOT = process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || '.'
13
+ // El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
14
+ // de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
15
+ const ROOT = (process.env.OPS_ROOT
16
+ || `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
14
17
  const CONFIG = `${ROOT}/ops.config.json`
15
18
  const P = `${ROOT}/planning`
16
19
  const BACKLOG = `${P}/BACKLOG.md`
@@ -10,7 +10,10 @@ export const meta = {
10
10
  ],
11
11
  }
12
12
 
13
- const ROOT = process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || '.'
13
+ // El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
14
+ // de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
15
+ const ROOT = (process.env.OPS_ROOT
16
+ || `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
14
17
  const PROVIDER = process.env.OPS_INTEGRATION_PROVIDER || ''
15
18
  const KEY = process.env.OPS_INTEGRATION_KEY || ''
16
19
  const RESULT = {
@@ -10,7 +10,10 @@ export const meta = {
10
10
  ],
11
11
  }
12
12
 
13
- const ROOT = process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || '.'
13
+ // El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
14
+ // de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
15
+ const ROOT = (process.env.OPS_ROOT
16
+ || `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
14
17
  const REQUESTED = process.env.OPS_INTEGRATION_PROVIDER || ''
15
18
  const RESULT = {
16
19
  type: 'object', additionalProperties: false, required: ['passed', 'provider', 'details'],
@@ -15,7 +15,10 @@ export const meta = {
15
15
  ],
16
16
  }
17
17
 
18
- const ROOT = process.env.OPS_ROOT || process.env.CLAUDE_PROJECT_DIR || '.'
18
+ // El prefijo lo completa `automation install`: en modo sidecar la herramienta se abre en la carpeta
19
+ // de la compañía y la raíz ops es uno de sus hijos, no la carpeta misma.
20
+ const ROOT = (process.env.OPS_ROOT
21
+ || `${process.env.CLAUDE_PROJECT_DIR || '.'}/{{OPS_DIR}}`).replace(/\/+$/, '')
19
22
  const P = `${ROOT}/planning`
20
23
  const ROADMAP = `${P}/roadmap`
21
24
  const HUMAN = `${P}/HUMAN_ACTIONS.md`
@@ -122,13 +122,20 @@ function resolveItem(paths, root, name, item) {
122
122
  }
123
123
  }
124
124
 
125
- // La configuración del runner nombra cada guard por ruta relativa a donde se abre la herramienta.
126
- // Si eso ya no es la raíz ops, hay que reescribir el prefijo o el runner buscaría los guards en la
127
- // carpeta equivocada. Se hace en un solo lugar: `install` lo escribe y `doctor` compara contra esto.
125
+ // Todo lo que un adaptador copia —configuración, instrucciones, workflows— nombra rutas relativas a
126
+ // la carpeta donde se abre la herramienta. Cuando la raíz ops no es esa carpeta, cada una necesita el
127
+ // prefijo. El marcador es explícito en la fuente en vez de adivinarse con reemplazos de texto:
128
+ // `{{OPS_DIR}}` significa "acá va la raíz ops, o nada si coinciden".
129
+ //
130
+ // Un solo render, y `install` escribe exactamente lo que `doctor` compara.
131
+ const OPS_DIR = '{{OPS_DIR}}'
132
+
133
+ function render(file, prefix) {
134
+ return fs.readFileSync(file, 'utf8').split(OPS_DIR).join(prefix)
135
+ }
136
+
128
137
  function runnerConfig(paths, root) {
129
- const raw = fs.readFileSync(paths.configSource, 'utf8')
130
- const prefix = opsPrefix(root)
131
- return JSON.parse(prefix ? raw.split('automatization/hooks/').join(`${prefix}automatization/hooks/`) : raw)
138
+ return JSON.parse(render(paths.configSource, opsPrefix(root)))
132
139
  }
133
140
 
134
141
  // Cargos del catálogo, con el frontmatter que el runner indexa para elegir a quién invocar.
@@ -368,11 +375,15 @@ function doctor(root, name, output = console) {
368
375
  }
369
376
  for (const item of runner.instructions || []) {
370
377
  const resolved = { item, ...resolveItem(paths, root, name, item) }
378
+ // `AGENTS.md` es un nombre compartido entre herramientas y la instancia ya tiene el suyo: en
379
+ // modo embedded el archivo del runner y el de la empresa son el mismo, y pedirle que se
380
+ // referencie a sí mismo no significa nada.
381
+ const propio = path.basename(resolved.target) === 'AGENTS.md'
371
382
  if (!fs.existsSync(resolved.target)) errors.push(`falta ${item.target}`)
372
- else if (!fs.readFileSync(resolved.target, 'utf8').includes('AGENTS.md')) {
383
+ else if (!propio && !fs.readFileSync(resolved.target, 'utf8').includes('AGENTS.md')) {
373
384
  warnings.push(`${item.target}: no referencia AGENTS.md; verifica las reglas globales`)
374
385
  }
375
- if (deliveryState(M.readRunners(root), name, resolved) === 'desactualizado') {
386
+ if (deliveryState(M.readRunners(root), name, resolved, opsPrefix(root)) === 'desactualizado') {
376
387
  warnings.push(`${item.target}: Cauce trae una versión más nueva y vos no lo tocaste; reinstalá`)
377
388
  }
378
389
  }
@@ -381,7 +392,7 @@ function doctor(root, name, output = console) {
381
392
  const recorded = M.readRunners(root)
382
393
  for (const item of runner.artifacts || []) {
383
394
  const resolved = { item, ...resolveItem(paths, root, name, item) }
384
- const situacion = deliveryState(recorded, name, resolved)
395
+ const situacion = deliveryState(recorded, name, resolved, opsPrefix(root))
385
396
  if (situacion === 'nuevo') errors.push(`falta ${item.target}`)
386
397
  else if (situacion === 'desactualizado') {
387
398
  warnings.push(`${item.target}: hay una versión más nueva en Cauce; reinstalá el adaptador`)
@@ -432,10 +443,10 @@ function deliveryKey(name, target) {
432
443
  //
433
444
  // Sin el registro de entrega, `desactualizado` y `ajeno` se ven igual. `install` resolvía esa duda
434
445
  // conservando siempre, así que ninguna mejora del toolkit llegaba nunca a un runner ya instalado.
435
- function deliveryState(recorded, name, resolved) {
446
+ function deliveryState(recorded, name, resolved, prefix = '') {
436
447
  if (!fs.existsSync(resolved.target)) return 'nuevo'
437
448
  const current = M.digest(resolved.target)
438
- if (current === M.digest(resolved.source)) return 'al día'
449
+ if (current === M.digestText(render(resolved.source, prefix))) return 'al día'
439
450
  const delivered = recorded[deliveryKey(name, resolved.item.target)]
440
451
  return delivered && delivered === current ? 'desactualizado' : 'ajeno'
441
452
  }
@@ -458,7 +469,8 @@ function install(root, name, output = console, options = {}) {
458
469
  for (const resolved of resolvedItems) F.assertNoSymlinkPath(paths.install, resolved.target)
459
470
 
460
471
  const recorded = M.readRunners(root)
461
- const state = new Map(resolvedItems.map((resolved) => [resolved, deliveryState(recorded, name, resolved)]))
472
+ const prefix = opsPrefix(root)
473
+ const state = new Map(resolvedItems.map((r) => [r, deliveryState(recorded, name, r, prefix)]))
462
474
  // Un archivo de instrucciones es del proyecto y se conserva; uno ejecutable es del toolkit y se
463
475
  // reemplaza. Editarlo detiene la instalación antes de pisarlo, igual que hace `upgrade`.
464
476
  const editados = resolvedItems.filter((resolved) => {
@@ -480,7 +492,7 @@ function install(root, name, output = console, options = {}) {
480
492
  // Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
481
493
  // el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
482
494
  if (paths.install !== root) {
483
- output.log(` ${name}: el runner se abre en ${paths.install} — ahí quedan .claude/ y CLAUDE.md`)
495
+ output.log(` ${name}: el runner se abre en ${paths.install} — ahí queda su configuración`)
484
496
  }
485
497
  output.log(`✓ ${name}: configuración instalada en ${runner.config.target}`)
486
498
  const entregado = { ...recorded }
@@ -493,7 +505,7 @@ function install(root, name, output = console, options = {}) {
493
505
  output.log(`= ${name}: ${resolved.item.target} ya está al día`)
494
506
  } else {
495
507
  fs.mkdirSync(path.dirname(resolved.target), { recursive: true })
496
- fs.copyFileSync(resolved.source, resolved.target)
508
+ F.atomicWrite(resolved.target, render(resolved.source, opsPrefix(root)))
497
509
  const verbo = situacion === 'nuevo' ? 'instalado' : 'actualizado'
498
510
  output.log(`✓ ${name}: ${verbo} ${resolved.item.target}`)
499
511
  }
package/engine/cli/ops.js CHANGED
@@ -167,7 +167,7 @@ function init(target) {
167
167
  let entregado = {}
168
168
  for (const relative of O.trackedPaths()) {
169
169
  const dir = path.join(root, relative)
170
- if (fs.existsSync(dir)) entregado = { ...entregado, ...M.record(root, relative, O.treeFiles(dir)) }
170
+ if (fs.existsSync(dir)) entregado = M.record(root, relative, O.treeFiles(dir), entregado)
171
171
  }
172
172
  M.write(root, entregado)
173
173
  // La instancia recuerda de qué versión salió: sin esto no hay actualización posible.
@@ -563,7 +563,7 @@ function upgrade(dir) {
563
563
  let registro = M.read(root)
564
564
  for (const relative of O.trackedPaths()) {
565
565
  const dir = path.join(root, relative)
566
- if (fs.existsSync(dir)) registro = { ...registro, ...M.record(root, relative, O.treeFiles(dir)) }
566
+ if (fs.existsSync(dir)) registro = M.record(root, relative, O.treeFiles(dir), registro)
567
567
  }
568
568
  M.write(root, M.prune(root, registro))
569
569
 
@@ -594,9 +594,15 @@ function upgrade(dir) {
594
594
 
595
595
  // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
596
596
  // reimplemente el recorrido del catálogo.
597
+ // La raíz ops de un comando que no la recibe. El shim `tools/ops.js` la exporta porque sabe dónde
598
+ // vive: sin eso, invocarlo desde otra carpeta —lo normal en sidecar— la resolvía contra el cwd.
599
+ function opsRoot(dir) {
600
+ return path.resolve(dir || process.env.OPS_ROOT || '.')
601
+ }
602
+
597
603
  function agents(action, dir) {
598
604
  if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
599
- const root = path.resolve(dir || '.')
605
+ const root = opsRoot(dir)
600
606
  const roles = AG.list(root)
601
607
  if (process.argv.includes('--json')) {
602
608
  // `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
@@ -685,7 +691,7 @@ async function integration(action, rootArg, provider, key) {
685
691
  }
686
692
 
687
693
  function automation(action, rootArg, runnerName) {
688
- const root = path.resolve(rootArg || '.')
694
+ const root = opsRoot(rootArg)
689
695
  if (action === 'list-hooks') return A.listHooks()
690
696
  if (action === 'list') {
691
697
  for (const name of A.RUNNER_NAMES) {
@@ -756,12 +762,12 @@ function evaluate(agent) {
756
762
 
757
763
  function team(action, slug) {
758
764
  if (action === 'list') {
759
- for (const name of T.list(process.cwd())) console.log(name)
765
+ for (const name of T.list(opsRoot())) console.log(name)
760
766
  return
761
767
  }
762
768
  if (!['check', 'show'].includes(action)) fail(`Acción de team desconocida: ${action || '(vacía)'}`, 2)
763
769
  try {
764
- const result = T.validate(process.cwd(), slug)
770
+ const result = T.validate(opsRoot(), slug)
765
771
  for (const error of result.errors) console.error(`✗ ${error}`)
766
772
  if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
767
773
  if (action === 'show') {
@@ -13,10 +13,12 @@ const path = require('node:path')
13
13
 
14
14
  const FILE = path.join('.cauce', 'manifest.json')
15
15
 
16
+ function digestText(content) {
17
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16)
18
+ }
19
+
16
20
  function digest(file) {
17
- try {
18
- return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex').slice(0, 16)
19
- } catch { return '' }
21
+ try { return digestText(fs.readFileSync(file)) } catch { return '' }
20
22
  }
21
23
 
22
24
  // Dos secciones porque son dos entregas distintas: `files` es lo que se materializó dentro de la
@@ -53,9 +55,12 @@ function write(root, files, runners) {
53
55
  fs.writeFileSync(target, `${JSON.stringify(data, null, 2)}\n`)
54
56
  }
55
57
 
56
- // Registra lo entregado en una ruta, relativo a la raíz de la instancia.
57
- function record(root, relative, files) {
58
- const current = read(root)
58
+ // Registra lo entregado en una ruta, relativo a la raíz de la instancia. Acumula sobre lo que recibe
59
+ // y no vuelve al disco: releerlo en cada ruta hacía que la última anulara a todas las anteriores,
60
+ // así que tras un `upgrade` casi todos los digests quedaban viejos y la actualización siguiente los
61
+ // leía como ediciones de la empresa. Un callejón sin salida sin que nadie hubiera tocado nada.
62
+ function record(root, relative, files, into = {}) {
63
+ const current = { ...into }
59
64
  for (const file of files) current[`${relative}/${file}`] = digest(path.join(root, relative, file))
60
65
  return current
61
66
  }
@@ -80,4 +85,4 @@ function edited(root, relative, files) {
80
85
  })
81
86
  }
82
87
 
83
- module.exports = { FILE, digest, edited, prune, read, readRunners, record, write }
88
+ module.exports = { FILE, digest, digestText, edited, prune, read, readRunners, record, write }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -8,6 +8,12 @@
8
8
  const path = require('path')
9
9
 
10
10
  const root = path.join(__dirname, '..')
11
+
12
+ // El shim sabe dónde vive; quien lo invoca, no. En modo sidecar se lo llama desde la carpeta de la
13
+ // compañía —`node <empresa>-ops/tools/ops.js …`— y sin esto cada comando resolvería su raíz contra
14
+ // el cwd: `agents list` y `team list` devolvían vacío en vez de fallar.
15
+ process.env.OPS_ROOT = process.env.OPS_ROOT || root
16
+
11
17
  const candidates = [
12
18
  () => require.resolve('@ingeniomaps/cauce/engine/cli/ops.js', { paths: [root] }),
13
19
  () => require.resolve(path.join(root, '.ops', 'engine', 'cli', 'ops.js')),