@ingeniomaps/cauce 0.95.0 → 0.96.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,48 @@ 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.96.0] - 2026-09-16
18
+
19
+ ### Corregido
20
+
21
+ - **Tres errores que el CLI contestaba con exit 1 ahora contestan 2, que es lo que dice la convención.**
22
+ Son «no se llegó a la pregunta»: `integrations/config.json` ilegible, un proveedor que no está en ese
23
+ registro, y un `package.json` inválido al declarar el motor. Sus hermanos —un `ops.config.json`
24
+ ilegible, un proveedor que Cauce no trae— ya contestaban 2.
25
+
26
+ El corte, que hasta ahora estaba en la cabeza de quien escribía cada salida, está escrito y sale en
27
+ `ops --help`: **2** es que el comando no existe, falta un argumento, o la raíz que nombrás no es lo que
28
+ dice ser —lo arreglás cambiando la invocación—; **1** es que se llegó y la respuesta es que no —una
29
+ validación encontró problemas, el estado se niega, o una operación falló a mitad de camino—.
30
+
31
+ Si tenés un script que distingue los dos, revisá esos tres casos. Si sólo mira «distinto de cero», no
32
+ cambia nada.
33
+
34
+ - **`ops flow list|check|show` acepta la raíz de la instancia, como el resto.** Todos los comandos que leen
35
+ una instancia la toman como posicional; `flow` la descartaba y resolvía por el directorio actual, así que
36
+ `ops flow list <raíz>` contestaba sobre otra cosa — y una lista de recorridos se lee igual de bien venga
37
+ de donde venga. Ahora es `ops flow list [ops-root]`, y con recorrido va después de él.
38
+
39
+ - **Una lista de cargos o recorridos vacía dice si es porque no hay o porque no se pudo resolver el
40
+ paquete.** Eran dos hechos distintos con la misma respuesta, y las acciones son opuestas. El aviso sale
41
+ por `stderr` y sólo cuando la lista viene vacía; el código de salida y el `--json` no cambian, porque
42
+ ese JSON lo consume el cron del ciclo de aprendizaje.
43
+
44
+ - **`automation check` nombra un `ops.config.json` ilegible en vez de culpar al motor.** La resolución del
45
+ paquete cuelga de esa configuración, así que un JSON roto se propagaba como archivos del motor que faltan
46
+ y el aviso mandaba a correr `npm install` sobre un motor instalado: la acción sugerida no arreglaba nada.
47
+
48
+ - **El catálogo de cargos y recorridos se encuentra también cuando el paquete vive un nivel arriba.** Es el
49
+ layout que documenta el arranque —`npm install @ingeniomaps/cauce` en la carpeta de la empresa y la
50
+ instancia adentro—, y 0.92.0 lo arregló para el motor y dejó afuera el catálogo. En ese estado `check`
51
+ pasaba en verde y `agents list` contestaba **una lista vacía con exit 0**, indistinguible de «esta
52
+ instancia no tiene cargos»; `flow list` igual, y `evaluate` culpaba a un `SKILL.md` que falta, mandando
53
+ a mirar el cargo en vez del paquete.
54
+
55
+ Si te pasó, no hacía falta bajar una segunda copia: con esta versión los cargos aparecen donde ya estaban.
56
+ Verificado instalando el paquete publicado desde el registro y recorriendo la superficie del CLI: de 0
57
+ cargos y 0 recorridos a **53 y 7**, sobre la misma instancia.
58
+
17
59
  ## [0.95.0] - 2026-09-16
18
60
 
19
61
  ### Corregido
@@ -22,6 +22,12 @@ const { hasHooks } = require('./config')
22
22
 
23
23
  function check(root) {
24
24
  const errors = []
25
+ // La configuración se lee primero y se corta ahí. La resolución del paquete cuelga de ella —`declaredRoot`
26
+ // la parsea— así que un JSON roto se propagaba como archivos del motor que faltan, y el aviso mandaba a
27
+ // correr `npm install` sobre un motor que está instalado: la acción sugerida no arreglaba nada. Nombrar
28
+ // la causa y no seguir es lo que separa este aviso del ruido — enumerar ausencias que salen de una causa
29
+ // ya nombrada manda a arreglar lo que no está roto (caso 174).
30
+ try { O.mode(root) } catch (error) { return [error.message] }
25
31
  const hookDir = path.join(root, 'automatization', 'hooks')
26
32
  if (!fs.existsSync(path.join(root, 'automatization', 'AGENTS.md'))) {
27
33
  errors.push('falta automatization/AGENTS.md')
@@ -11,7 +11,7 @@ const P = require('../planning/parser')
11
11
  const PC = require('../planning/contracts')
12
12
  const AD = require('../planning/adoption')
13
13
  const F = require('../core/files')
14
- const { fail, planningRoot, TODAY } = require('./io')
14
+ const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
15
15
 
16
16
  // El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
17
17
  // a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
@@ -30,7 +30,7 @@ function adopt(dir) {
30
30
  const existing = fs.readFileSync(target, 'utf8')
31
31
  if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
32
32
  fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
33
- + 'delante; `check` marca los que ya cumplen.')
33
+ + 'delante; `check` marca los que ya cumplen.', REFUSED)
34
34
  }
35
35
  const slugs = AD.declared(existing)
36
36
  F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
@@ -78,7 +78,7 @@ function archiveHumanActions(root) {
78
78
  function archive(dir, rawNum) {
79
79
  if (String(rawNum || '') === 'human-actions') return archiveHumanActions(planningRoot(dir))
80
80
  return fail('Sólo se archiva `human-actions`. La evidencia de una tarea ya vive en su propio archivo '
81
- + 'de `done/`, así que archivar una épica dejó de tener sentido.', 2)
81
+ + 'de `done/`, así que archivar una épica dejó de tener sentido.', USAGE)
82
82
  }
83
83
 
84
84
  module.exports = { archive, adopt }
@@ -18,7 +18,7 @@ const CL = require('../planning/claims')
18
18
  const P = require('../planning/parser')
19
19
  const IN = require('./instance')
20
20
  const O = require('../core/ownership')
21
- const { fail, opsRoot, TODAY } = require('./io')
21
+ const { fail, opsRoot, TODAY, USAGE } = require('./io')
22
22
 
23
23
  // Qué decir cuando el banco sobrevivió a su propio borrado, que es lo único que va a permitir
24
24
  // establecer la causa. Devuelve el mensaje en vez de escribirlo donde ocurre, y eso es lo que lo hace
@@ -106,7 +106,7 @@ function makeBench(root, dir, force, name) {
106
106
  const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' })
107
107
  if ((dirty.stdout || '').trim() && !force) {
108
108
  fail(`${dir} tiene trabajo sin recoger. Guardá lo que esa corrida dejó antes de rehacerlo, `
109
- + 'o usá --force si ya lo tenés.', 2)
109
+ + 'o usá --force si ya lo tenés.', USAGE)
110
110
  }
111
111
  // Rodear un borrado a medias deja la corrida siguiendo sobre un banco que no es nuevo, y lo que falla
112
112
  // después no dice nada del borrado: el test que lo destapó reportaba `true !== false` sobre un archivo
@@ -116,7 +116,7 @@ function makeBench(root, dir, force, name) {
116
116
  // **Y de acá para abajo el directorio no existe.** Eso es lo que sostiene que el andamiaje y el enlace
117
117
  // se escriban sin defensas: hasta el 073, los dos llevaban una por si algo sobrevivía al borrado.
118
118
  const problema = clearBench(dir, path.join(root, '.cauce-eval'))
119
- if (problema) fail(problema, 2)
119
+ if (problema) fail(problema, USAGE)
120
120
  // Sin `force`, y eso es lo que hay que poder decir: sólo servía si algún archivo sobrevivía al borrado,
121
121
  // y la comprobación de arriba garantiza que no queda ninguno. Lo llevaba porque el mismo test falló tres
122
122
  // veces en un día con «El destino contiene …/AGENTS.md», y eso era el escritor de fondo que apagó el 073.
@@ -183,7 +183,7 @@ function seal(dir, git, mensaje) {
183
183
  function evaluationBench(root, agent, caso, force, kind) {
184
184
  const safe = (value) => {
185
185
  if (!/^[a-z0-9_][a-z0-9._-]*$/i.test(value) || value.includes('..')) {
186
- fail(`nombre inválido para el banco: ${value}`, 2)
186
+ fail(`nombre inválido para el banco: ${value}`, USAGE)
187
187
  }
188
188
  return value
189
189
  }
@@ -273,7 +273,7 @@ function populate(dir, scenario, git) {
273
273
  // `clearBench` ya se niega a borrar fuera de ahí— y se distingue por el escenario, que es lo que lo puebla.
274
274
  function measurementBench(root, scenario, force) {
275
275
  if (!SCENARIOS.includes(scenario)) {
276
- fail(`escenario desconocido: ${scenario || '(ninguno)'}. Hay ${SCENARIOS.join(', ')}.`, 2)
276
+ fail(`escenario desconocido: ${scenario || '(ninguno)'}. Hay ${SCENARIOS.join(', ')}.`, USAGE)
277
277
  }
278
278
  const dir = path.join(root, '.cauce-eval', '_medicion', scenario)
279
279
  const { env, git } = makeBench(root, dir, force, `Banco de medición (${scenario})`)
@@ -293,7 +293,7 @@ function bench(scenario, cli) {
293
293
  const root = opsRoot()
294
294
  if (O.mode(root) !== 'toolkit') {
295
295
  fail('ops bench es del toolkit: arma un banco desechable para medir a Cauce. En una instancia, lo '
296
- + 'que se mide es tu propio proyecto — corré el comando que quieras medir sobre tu planning/.', 2)
296
+ + 'que se mide es tu propio proyecto — corré el comando que quieras medir sobre tu planning/.', USAGE)
297
297
  }
298
298
  const dir = measurementBench(root, scenario, cli.has('--force'))
299
299
  console.log(path.relative(root, dir))
@@ -13,14 +13,14 @@ const O = require('../core/ownership')
13
13
  // El banco desechable y su borrado comprobado. Se reexportan abajo porque su contrato lo fija la suite
14
14
  // del banco, que llega por acá desde antes de que el módulo existiera.
15
15
  const B = require('./bench')
16
- const { fail, opsRoot, TODAY } = require('./io')
16
+ const { fail, opsRoot, TODAY, USAGE, REFUSED } = require('./io')
17
17
 
18
18
  function agentsFork(slug, dir) {
19
19
  const root = opsRoot(dir)
20
- if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', 2)
20
+ if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', USAGE)
21
21
  let result
22
22
  const date = TODAY()
23
- try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message, 2) }
23
+ try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message, USAGE) }
24
24
  console.log(`+ ${path.relative(root, result.dir)} (${result.files.length} archivo(s))`)
25
25
  if (result.skipped.length) {
26
26
  console.log(` quedan en el catálogo: ${result.skipped.length} artefacto(s) que ganó su versión`)
@@ -31,15 +31,28 @@ function agentsFork(slug, dir) {
31
31
 
32
32
  // Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
33
33
  // reimplemente el recorrido del catálogo.
34
+ // Una lista vacía y un paquete que no se resolvió se ven igual, y las acciones son opuestas: en uno no hay
35
+ // nada que hacer, en el otro falta una instalación. Se avisa sólo cuando la lista sale vacía, que es cuando
36
+ // la ambigüedad existe — con resultados a la vista no hay nada que aclarar.
37
+ //
38
+ // Va por `stderr` y no por el código de salida: el `--json` lo consume el cron del ciclo de aprendizaje, y
39
+ // romperle el contrato para arreglar un mensaje sería cambiar lo que no está mal (caso 173).
40
+ function warnUnresolved(root, name, found) {
41
+ if (found || O.packageDir(root, name)) return
42
+ console.error(`⚠ ${name}/ no se pudo resolver desde ${root}: el paquete @ingeniomaps/cauce no está ahí `
43
+ + `ni en la raíz que declara ops.config.json, así que una lista vacía acá no dice que no haya ${name}.`)
44
+ }
45
+
34
46
  function agents(action, dir, extra, cli) {
35
47
  if (action === 'fork') return agentsFork(dir, extra)
36
- if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, 2)
48
+ if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, USAGE)
37
49
  const root = opsRoot(dir)
38
50
  // Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
39
51
  // recorrer el catálogo entero para encontrar el suyo es ruido. `--own` hace ejecutable ese recorrido.
40
52
  const own = cli.has('--own')
41
53
  const system = cli.has('--system')
42
54
  const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
55
+ warnUnresolved(root, 'agents', roles.length)
43
56
  if (cli.has('--json')) {
44
57
  // `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
45
58
  return console.log(JSON.stringify(roles.map((role) => ({
@@ -104,7 +117,7 @@ function learn(agent, cli) {
104
117
  // falta. Negarse nombrando el comando que sí corresponde es lo que cierra R13.
105
118
  if (kind === 'flow' && !cli.has('--proposal')) {
106
119
  fail(`${agent} es un recorrido: aprende de sus corridas, no de informes semanales.\n`
107
- + ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, 2)
120
+ + ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, USAGE)
108
121
  }
109
122
  // `--period` es para consolidar a mano un mes que no es el de hoy. El ciclo automático no lo
110
123
  // pasa: la propuesta se llama por el mes en que se abre y arrastra lo que todavía no entró.
@@ -133,7 +146,7 @@ function learn(agent, cli) {
133
146
  if (LF.blankProposal(result.file)) {
134
147
  console.log(' sin cambio decidido: falta correr agent-propose antes de que esto se pueda firmar')
135
148
  }
136
- } catch (error) { fail(error.message, 2) }
149
+ } catch (error) { fail(error.message, USAGE) }
137
150
  }
138
151
 
139
152
  function evaluate(agent, caso, cli) {
@@ -149,7 +162,7 @@ function evaluate(agent, caso, cli) {
149
162
  // forkeaba, repetía el comando y recibía el mismo mensaje diciéndole que forkeara—.
150
163
  fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: corré '
151
164
  + `"ops evaluate ${agent}" sin la bandera, que valida sus controles, casos y propuestas `
152
- + 'contra este proyecto.', 2)
165
+ + 'contra este proyecto.', USAGE)
153
166
  }
154
167
  // El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
155
168
  // un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
@@ -190,7 +203,7 @@ function evaluate(agent, caso, cli) {
190
203
  const errors = [...result.errors, ...runs.errors]
191
204
  for (const warning of [...result.warnings, ...runs.warnings]) console.warn(`⚠ ${warning}`)
192
205
  for (const error of errors) console.error(`✗ ${error}`)
193
- if (errors.length) fail(`\n${errors.length} error(es)`, 1)
206
+ if (errors.length) fail(`\n${errors.length} error(es)`, 1, REFUSED)
194
207
  // Cuándo se midió es un rango cuando el veredicto vigente lo aportó más de una corrida. Por qué se
195
208
  // compone en vez de leerse la última, en `composed`.
196
209
  const measuredAt = runs.state && runs.state.oldest !== runs.state.newest
@@ -208,13 +221,19 @@ function evaluate(agent, caso, cli) {
208
221
  `${result.pending ? ` (${result.pending} sin aplicar)` : ''}, ` +
209
222
  'controles estructurales válidos',
210
223
  )
211
- } catch (error) { fail(error.message, 2) }
224
+ } catch (error) { fail(error.message, USAGE) }
212
225
  }
213
226
 
214
- function flow(action, slug, cli) {
227
+ // La raíz la toma como posicional, igual que el resto de los comandos que leen una instancia: `list` no
228
+ // lleva recorrido, así que la suya es el primer posicional, y `check`/`show` la llevan después del
229
+ // recorrido —la misma forma que `agents fork <cargo> [ops-root]`—. Antes se descartaba y todo resolvía por
230
+ // `opsRoot()`, así que `ops flow list <raíz>` contestaba sobre el directorio actual y la respuesta se leía
231
+ // igual de bien viniendo de otra instancia (caso 172).
232
+ function flow(action, slug, dir, cli) {
233
+ const root = opsRoot(action === 'list' ? slug : dir)
215
234
  if (action === 'list') {
216
- const root = opsRoot()
217
235
  const slugs = T.list(root)
236
+ warnUnresolved(root, 'flows', slugs.length)
218
237
  // La misma forma que `agents list --json`, porque la consume el mismo cron. `cadence` es fija:
219
238
  // un recorrido no tiene profesión que cambie afuera, así que su calendario no se deriva de
220
239
  // fuentes —no tiene—; lo que decide si le toca es `pending`, las corridas que dejó sin consolidar.
@@ -238,11 +257,11 @@ function flow(action, slug, cli) {
238
257
  for (const name of slugs) console.log(name)
239
258
  return
240
259
  }
241
- if (!['check', 'show'].includes(action)) fail(`Acción de flow desconocida: ${action || '(vacía)'}`, 2)
260
+ if (!['check', 'show'].includes(action)) fail(`Acción de flow desconocida: ${action || '(vacía)'}`, USAGE)
242
261
  try {
243
- const result = T.validate(opsRoot(), slug)
262
+ const result = T.validate(root, slug)
244
263
  for (const error of result.errors) console.error(`✗ ${error}`)
245
- if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
264
+ if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1, REFUSED)
246
265
  if (action === 'show') {
247
266
  // El manifiesto entero, para que un workflow ejecute las etapas sin que un modelo lo parsee.
248
267
  if (cli.has('--json')) return console.log(JSON.stringify(result.manifest))
@@ -254,7 +273,7 @@ function flow(action, slug, cli) {
254
273
  } else {
255
274
  console.log(`✓ ${slug}: ${result.stages} etapa(s), ${result.agents} agente(s), contrato válido`)
256
275
  }
257
- } catch (error) { fail(error.message, 2) }
276
+ } catch (error) { fail(error.message, USAGE) }
258
277
  }
259
278
 
260
279
  module.exports = {
@@ -9,25 +9,25 @@ const path = require('node:path')
9
9
  const CL = require('../planning/claims')
10
10
  const R = require('../core/repos')
11
11
  const ST = require('../planning/state')
12
- const { fail, planningRoot, TODAY } = require('./io')
12
+ const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
13
13
 
14
14
  function claim(dir, slug, cli) {
15
15
  const root = planningRoot(dir)
16
- if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`', 2)
16
+ if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`', USAGE)
17
17
  const state = ST.snapshot(root)
18
18
  const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
19
- if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`, 2)
19
+ if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`, USAGE)
20
20
 
21
21
  // No se reserva lo que todavía no se puede empezar: una tarea tomada con su dependencia en vuelo
22
22
  // bloquea la cola sin que nadie pueda avanzarla, y el runner que la tomó se queda sin poder tomar otra.
23
23
  const blocker = task.depends.find((dep) => !state.done.set.has(dep))
24
- if (blocker) return fail(`${slug} depende de ${blocker}, que todavía no está en DONE.`)
24
+ if (blocker) return fail(`${slug} depende de ${blocker}, que todavía no está en DONE.`, REFUSED)
25
25
 
26
26
  const me = CL.owner(root)
27
27
  const from = CL.runner()
28
28
  if (!me) {
29
29
  return fail('No sé quién sos. Configurá `git config user.email` o exportá CAUCE_OWNER: '
30
- + 'un reclamo anónimo no dice a quién preguntarle.', 2)
30
+ + 'un reclamo anónimo no dice a quién preguntarle.', USAGE)
31
31
  }
32
32
  // Un runner lleva una tarea a la vez (BR-OPS-001), y exigirlo acá hace ruidosa la única forma en que
33
33
  // este contrato falla en silencio: dos agentes de la misma máquina compartiendo id porque nadie puso
@@ -38,7 +38,7 @@ function claim(dir, slug, cli) {
38
38
  .find((one) => one.runner === from && one.slug !== slug && !state.done.set.has(one.slug))
39
39
  if (ocupado) {
40
40
  return fail(`este runner ya tiene ${ocupado.slug}. Cerrala o soltala primero; y si sos otro agente `
41
- + 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.')
41
+ + 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.', REFUSED)
42
42
  }
43
43
 
44
44
  const target = CL.file(root, slug)
@@ -58,7 +58,7 @@ function claim(dir, slug, cli) {
58
58
  const holder = CL.read(root).find((one) => one.slug === slug)
59
59
  // Existía al crear y ya no está: alguien la soltó entre las dos operaciones. Es una ventana de
60
60
  // microsegundos y aun así tiene respuesta, porque la alternativa es reventar con un TypeError.
61
- if (!holder) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`)
61
+ if (!holder) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`, REFUSED)
62
62
  if (holder.runner === from) return console.log(`= ${slug} ya era tuya desde ${holder.started}`)
63
63
  // Mismo dueño y otro runner son dos situaciones que se ven idénticas desde acá —vos retomando la
64
64
  // sesión de ayer, o un segundo agente tuyo— y ninguna se puede distinguir mirando el archivo.
@@ -68,10 +68,10 @@ function claim(dir, slug, cli) {
68
68
  return fail(`${slug} la tenés vos, tomada el ${holder.started} desde el runner ${holder.runner}. `
69
69
  + 'Preguntá si se retoma esa sesión —y entonces corré con ese id— o si es otro agente en '
70
70
  + `paralelo, que toma otra tarea. \`ops runners ${path.relative(process.cwd(), root) || '.'}\` `
71
- + 'lista lo que hay abierto.')
71
+ + 'lista lo que hay abierto.', REFUSED)
72
72
  }
73
73
  return fail(`${slug} la tomó ${holder.owner} el ${holder.started}. Si se abandonó, borrá `
74
- + `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
74
+ + `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`, REFUSED)
75
75
  }
76
76
  console.log(`✓ ${slug} tomada por ${me}`)
77
77
  if (!cli.has('--json')) {
@@ -87,12 +87,12 @@ function claim(dir, slug, cli) {
87
87
 
88
88
  function release(dir, slug) {
89
89
  const root = planningRoot(dir)
90
- if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`', 2)
90
+ if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`', USAGE)
91
91
  const from = CL.runner()
92
92
  const taken = CL.read(root).find((one) => one.slug === slug)
93
- if (!taken) return fail(`${slug} no está tomada por nadie.`, 2)
93
+ if (!taken) return fail(`${slug} no está tomada por nadie.`, USAGE)
94
94
  if (taken.runner !== from) {
95
- return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano.`)
95
+ return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano.`, REFUSED)
96
96
  }
97
97
  fs.rmSync(CL.file(root, slug))
98
98
  console.log(`✓ ${slug} soltada; volvió a la cola`)
@@ -15,7 +15,7 @@
15
15
  const fs = require('node:fs')
16
16
  const path = require('node:path')
17
17
  const P = require('../planning/parser')
18
- const { fail, opsRoot } = require('./io')
18
+ const { fail, opsRoot, USAGE } = require('./io')
19
19
 
20
20
  // Los cuatro que componen el contrato, con la ruta relativa a la raíz de la instancia. El orden es el que
21
21
  // usa el mensaje de error: se nombra el primero que falte y no los cuatro, porque arreglar uno suele
@@ -188,14 +188,15 @@ function contract(dir, cli) {
188
188
  // se pierde no se ve en la salida, se ve tres fases después en lo que un subagente creyó que podía tocar.
189
189
  if (missing) {
190
190
  return fail(`${root} no tiene ${missing}, así que no hay contrato que derivar. Es la raíz que escribió `
191
- + '`automation install`: comprobá que exista y, si moviste el proyecto de carpeta, reinstalá el adaptador.', 2)
191
+ + '`automation install`: comprobá que exista y, si moviste el proyecto de carpeta, reinstalá el adaptador.',
192
+ USAGE)
192
193
  }
193
194
 
194
195
  let config
195
196
  try {
196
197
  config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
197
198
  } catch (error) {
198
- return fail(`ops.config.json no se pudo leer como JSON: ${error.message}`, 2)
199
+ return fail(`ops.config.json no se pudo leer como JSON: ${error.message}`, USAGE)
199
200
  }
200
201
 
201
202
  const sections = {}
@@ -205,7 +206,7 @@ function contract(dir, cli) {
205
206
  // que poder abrir el archivo y ver qué encabezado falta, y el arreglo es restaurarlo con `upgrade`.
206
207
  if (!found.trim()) {
207
208
  return fail(`${file} no tiene la sección ${name}, y de ahí sale el contrato que reciben los agentes. `
208
- + 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.', 2)
209
+ + 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.', USAGE)
209
210
  }
210
211
  sections[file] = found
211
212
  }
@@ -7,7 +7,7 @@
7
7
  const fs = require('node:fs')
8
8
  const path = require('node:path')
9
9
  const F = require('../core/files')
10
- const { fail } = require('./io')
10
+ const { fail, USAGE } = require('./io')
11
11
 
12
12
  // Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
13
13
  // Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
@@ -15,7 +15,7 @@ function declareEngine(manifest, version) {
15
15
  let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
16
16
  if (fs.existsSync(manifest)) {
17
17
  try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
18
- fail(`package.json inválido en ${manifest}: ${error.message}`)
18
+ fail(`package.json inválido en ${manifest}: ${error.message}`, USAGE)
19
19
  }
20
20
  }
21
21
  pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
@@ -15,7 +15,7 @@ const P = require('../planning/parser')
15
15
  const ST = require('../planning/state')
16
16
  const RC = require('../planning/recurring')
17
17
  const A = require('../automation')
18
- const { fail, TODAY } = require('./io')
18
+ const { fail, TODAY, USAGE, REFUSED } = require('./io')
19
19
  const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
20
20
  const { adviceFor, previewUpgrade, reportUpgrade } = require('./upgrade-report')
21
21
 
@@ -47,7 +47,7 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
47
47
  if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
48
48
  else {
49
49
  if (fs.existsSync(to)) {
50
- if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`)
50
+ if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`, REFUSED)
51
51
  if (!quiet) console.log(`= conservado ${to}`)
52
52
  let would = fs.readFileSync(from, 'utf8')
53
53
  for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
@@ -175,9 +175,9 @@ function whatIsLost(root) {
175
175
  function destroy(dir, cli) {
176
176
  const root = path.resolve(dir || '.')
177
177
  if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
178
- fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
178
+ fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, USAGE)
179
179
  }
180
- if (O.mode(root) === 'toolkit') fail(`${root} es el toolkit: acá se fabrica Cauce, no se lo borra.`, 2)
180
+ if (O.mode(root) === 'toolkit') fail(`${root} es el toolkit: acá se fabrica Cauce, no se lo borra.`, USAGE)
181
181
 
182
182
  const loss = whatIsLost(root)
183
183
  const lines = [
@@ -261,12 +261,12 @@ function renameTeamsToFlows(root) {
261
261
  function upgrade(dir, cli) {
262
262
  const root = path.resolve(dir || '.')
263
263
  if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
264
- fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, 2)
264
+ fail(`${root} no es una instancia de Cauce: falta ops.config.json.`, USAGE)
265
265
  }
266
266
  // Acá se fabrica Cauce: `upgrade` reemplazaría con las copias de `template/` los archivos que este
267
267
  // repositorio mantiene en la raíz —`AGENTS.md` entre ellos, que es donde vive esta misma regla—.
268
268
  if (O.mode(root) === 'toolkit') {
269
- fail(`${root} es el toolkit: acá se edita Cauce, no se lo actualiza.`, 2)
269
+ fail(`${root} es el toolkit: acá se edita Cauce, no se lo actualiza.`, USAGE)
270
270
  }
271
271
  // Antes que nada, y antes de los controles: `teams/` pasó a llamarse `flows/`, y los controles que
272
272
  // siguen miran las rutas nuevas. Sin esto `upgrade` copiaría `flows/` al lado y dejaría los
@@ -299,6 +299,7 @@ function upgrade(dir, cli) {
299
299
  fail(
300
300
  `\n${rescue.length} archivo(s) de aprendizaje quedaron en una ruta que Cauce ya no mantiene.\n\n` +
301
301
  'Movelos a un cargo propio en agents/roles/<slug>/learning/ y repetí, o descartalos con --force.',
302
+ REFUSED,
302
303
  )
303
304
  }
304
305
 
package/engine/cli/io.js CHANGED
@@ -3,9 +3,31 @@
3
3
  const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
 
6
+ // Los dos códigos con que termina el CLI, y el corte entre ellos. Están nombrados porque el número suelto
7
+ // no dice de qué lado cae: un «2» y un argumento ausente se leen igual de arbitrarios, así que cada sitio
8
+ // nuevo elegía por imitación del vecino y tres terminaron del lado equivocado.
9
+ //
10
+ // USAGE — no se llegó a la pregunta. El comando no existe, le falta un argumento, o la raíz que
11
+ // nombra no es lo que dice ser. Lo arregla quien invoca, cambiando la invocación.
12
+ // REFUSED — se llegó, y la respuesta es que no. Cubre tres formas y ninguna pide otro código: una
13
+ // validación encontró problemas, el estado se niega, o una operación falló a mitad de camino.
14
+ // La invocación estaba bien; lo que hay que mirar es el proyecto.
15
+ //
16
+ // La distinción se gana lo que cuesta porque afuera ya se la necesitaba sin tenerla: la parada
17
+ // `claim-stuck` de `autobuild` separa dos defectos leyendo el **texto** de lo que `claim` contestó, y por
18
+ // qué tiene que distinguirlos está ahí. Uno de los dos es exactamente USAGE.
19
+ //
20
+ // Un tercero no hace falta y costaría: las tres formas de REFUSED terminan igual para quien scriptea
21
+ // —mirá el proyecto—, y separarlas obligaría a conocer el reparto para hacer lo mismo con las tres.
22
+ const USAGE = 2
23
+ const REFUSED = 1
24
+
6
25
  // Terminar la corrida con un mensaje y un código. Vive aparte porque lo usa cada familia de comandos, y
7
26
  // dejarlo en el despacho obligaría a que cada módulo dependa del que lo invoca.
8
- function fail(message, code = 1) {
27
+ //
28
+ // El default se queda siendo REFUSED aunque la puerta exija que cada sitio diga el suyo: es el piso
29
+ // seguro —distinto de cero— para un llamador que la puerta todavía no mire.
30
+ function fail(message, code = REFUSED) {
9
31
  console.error(message)
10
32
  process.exit(code)
11
33
  }
@@ -40,14 +62,14 @@ function opsRoot(dir) {
40
62
  function planningRoot(dir) {
41
63
  const root = path.resolve(dir || '.')
42
64
  if (!fs.existsSync(root)) {
43
- return fail(`no existe el planning en ${root} (ruta resuelta). Comprobá desde dónde estás invocando.`, 2)
65
+ return fail(`no existe el planning en ${root} (ruta resuelta). Comprobá desde dónde estás invocando.`, USAGE)
44
66
  }
45
67
  // Existir no alcanza: un directorio cualquiera contestaría cola vacía igual. `BACKLOG.md` es el archivo
46
68
  // del que sale la cola, así que sin él la respuesta no significa nada.
47
69
  if (!fs.existsSync(path.join(root, 'BACKLOG.md'))) {
48
- return fail(`${root} (ruta resuelta) no es un planning: falta BACKLOG.md.`, 2)
70
+ return fail(`${root} (ruta resuelta) no es un planning: falta BACKLOG.md.`, USAGE)
49
71
  }
50
72
  return root
51
73
  }
52
74
 
53
- module.exports = { fail, opsRoot, planningRoot, TODAY }
75
+ module.exports = { fail, opsRoot, planningRoot, TODAY, USAGE, REFUSED }
package/engine/cli/ops.js CHANGED
@@ -6,7 +6,7 @@ const path = require('node:path')
6
6
  const { spawnSync } = require('node:child_process')
7
7
  const A = require('../automation')
8
8
  const { FLAGS, parse } = require('./args')
9
- const { fail } = require('./io')
9
+ const { fail, USAGE, REFUSED } = require('./io')
10
10
  const IN = require('./instance')
11
11
  const PL = require('./planning')
12
12
  const CT = require('./contract')
@@ -88,7 +88,7 @@ async function init(target, cli) {
88
88
  // directorio y elegían modos opuestos, así que escribir el punto daba el layout contrario al de
89
89
  // arriba sin que nadie lo pidiera.
90
90
  const mode = cli.value('--mode', isInstanceDir(root) ? 'sidecar' : 'embedded')
91
- if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', 2)
91
+ if (!['embedded', 'sidecar'].includes(mode)) fail('--mode debe ser embedded o sidecar.', USAGE)
92
92
  const name = cli.value('--name', defaultName(root))
93
93
  const force = cli.has('--force')
94
94
  // `.git` no cuenta como contenido: es lo único que hay en la carpeta que alguien acaba de crear y
@@ -96,7 +96,7 @@ async function init(target, cli) {
96
96
  // más natural —`mkdir acme-ops && git init && cauce init`— pedía `--force` para no pisar nada.
97
97
  const existing = (fs.existsSync(root) ? fs.readdirSync(root) : []).filter((entry) => entry !== '.git')
98
98
  if (existing.length && !force) {
99
- fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
99
+ fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`, REFUSED)
100
100
  }
101
101
  // Preguntar exige una terminal, e instalar baja un paquete y escribe `node_modules`: las dos cosas
102
102
  // pasan cuando hay alguien mirando. Una corrida automatizada —CI, un contenedor, estas pruebas—
@@ -112,7 +112,7 @@ async function init(target, cli) {
112
112
  }
113
113
  // Se valida antes de escribir: un runner o una integración que no existen no pueden dejar una
114
114
  // instancia hecha con el comando en error (caso 096). `BOOT.run` vuelve a validar con la misma función.
115
- try { BOOT.validate(options) } catch (error) { fail(error.message, 2) }
115
+ try { BOOT.validate(options) } catch (error) { fail(error.message, USAGE) }
116
116
  IN.scaffold(root, { name, mode, force })
117
117
  const relative = path.relative(process.cwd(), root)
118
118
  const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
@@ -125,7 +125,7 @@ async function init(target, cli) {
125
125
  installRunner: (runner) => W.automation('install', root, runner, NO_FLAGS),
126
126
  enableProvider: (provider) => W.enableProvider(root, provider),
127
127
  })
128
- } catch (error) { fail(error.message, 2) }
128
+ } catch (error) { fail(error.message, USAGE) }
129
129
 
130
130
  if (result.installed) VA.check(path.join(root, 'planning'), NO_FLAGS)
131
131
 
@@ -139,7 +139,7 @@ async function init(target, cli) {
139
139
  console.log('')
140
140
  W.onboard(root, NO_FLAGS, result.installed ? result.runner : '')
141
141
  for (const step of initSteps(enter, result)) console.log(step)
142
- if (result.error) fail(`${result.error}: la instancia quedó creada pero todavía no funciona.`)
142
+ if (result.error) fail(`${result.error}: la instancia quedó creada pero todavía no funciona.`, REFUSED)
143
143
  }
144
144
 
145
145
  function usage() {
@@ -184,9 +184,14 @@ function usage() {
184
184
  ops evaluate <agent|flow> [--flow] [--cases [--json]] [--bench [caso]] [--record [AAAA-MM-DD]]
185
185
  ops agents list [ops-root] [--own|--system] [--json]
186
186
  ops agents fork <cargo> [ops-root]
187
- ops flow list
188
- ops flow check <flow>
189
- ops flow show <flow>`)
187
+ ops flow list [ops-root]
188
+ ops flow check <flow> [ops-root]
189
+ ops flow show <flow> [ops-root]
190
+
191
+ Códigos de salida:
192
+ 2 no se llegó a la pregunta: el comando no existe, falta un argumento, o la raíz no es lo que dice ser.
193
+ 1 se llegó y la respuesta es que no: una validación encontró problemas, el estado se niega, o una
194
+ operación falló a mitad de camino.`)
190
195
  }
191
196
 
192
197
  // Un `cli` que no tiene banderas, para reusar un comando desde otro: el `--force` de `init` habla del
@@ -200,14 +205,14 @@ async function run(cli) {
200
205
  if (!command || command === 'help') return usage()
201
206
  // `Object.hasOwn` y no `FLAGS[command]`: `constructor` heredado de `Object.prototype` pasaba por
202
207
  // comando válido y el CLI salía con 0 sin hacer nada.
203
- if (!Object.hasOwn(FLAGS, command)) { usage(); fail(`Comando desconocido: ${command}`, 2) }
208
+ if (!Object.hasOwn(FLAGS, command)) { usage(); fail(`Comando desconocido: ${command}`, USAGE) }
204
209
  // `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
205
210
  // actual en vez de explicarse.
206
211
  if (cli.has('--help')) return usage()
207
212
  const unknown = cli.unknown(command)
208
213
  if (unknown.length) {
209
214
  const accepts = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
210
- fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`, 2)
215
+ fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`, USAGE)
211
216
  }
212
217
  const arg = cli.positional
213
218
  if (command === 'init') await init(arg[1], cli)
@@ -236,7 +241,7 @@ async function run(cli) {
236
241
  else if (command === 'automation') W.automation(arg[1], arg[2], arg[3], cli)
237
242
  else if (command === 'learn') CAT.learn(arg[1], cli)
238
243
  else if (command === 'evaluate') CAT.evaluate(arg[1], arg[2], cli)
239
- else if (command === 'flow') CAT.flow(arg[1], arg[2], cli)
244
+ else if (command === 'flow') CAT.flow(arg[1], arg[2], arg[3], cli)
240
245
  }
241
246
 
242
- run(parse(process.argv.slice(2))).catch((error) => fail(error.message))
247
+ run(parse(process.argv.slice(2))).catch((error) => fail(error.message, REFUSED))
@@ -14,7 +14,7 @@ const CL = require('../planning/claims')
14
14
  const ST = require('../planning/state')
15
15
  const O = require('../core/ownership')
16
16
  const EV = require('../core/evidence')
17
- const { fail, planningRoot, TODAY } = require('./io')
17
+ const { fail, planningRoot, TODAY, USAGE } = require('./io')
18
18
 
19
19
  // Qué dimensiones enumera el molde de `organization/` y cuáles dejaron de estar. Un agente que reescribe
20
20
  // esos archivos tiende a quedarse con el contenido y perder la estructura: el resultado se lee entero y
@@ -34,7 +34,7 @@ function evidence(dir, cli) {
34
34
  // Sin `--task`, la más reciente, y la decide `fecha:` — por qué ese campo existe lo dice el contrato.
35
35
  const reciente = [...entries].sort((a, b) => (a.fecha || '').localeCompare(b.fecha || '')).pop()
36
36
  const entry = slug ? entries.find((one) => one.slug === slug) : reciente
37
- if (!entry) return fail(slug ? `DONE no tiene la entrada ${slug}` : 'DONE no tiene ninguna entrada', 2)
37
+ if (!entry) return fail(slug ? `DONE no tiene la entrada ${slug}` : 'DONE no tiene ninguna entrada', USAGE)
38
38
 
39
39
  let config = {}
40
40
  try { config = JSON.parse(fs.readFileSync(path.join(opsDir, 'ops.config.json'), 'utf8')) } catch { /* sin raíces */ }
@@ -148,7 +148,7 @@ function context(dir, cli) {
148
148
  // Un hito mal escrito devolvería «sin tarea disponible», que es indistinguible de un hito terminado.
149
149
  if (!existe) {
150
150
  const hay = state.milestones.map((one) => one.slug).join(', ') || '(ninguno)'
151
- return fail(`el hito ${hito} no existe. Hay: ${hay}`, 2)
151
+ return fail(`el hito ${hito} no existe. Hay: ${hay}`, USAGE)
152
152
  }
153
153
  if (own) hitoOmitido = `${hito} no se aplica: ya tenés ${own.slug} tomada`
154
154
  else state.milestones = state.milestones.filter((one) => one.slug === hito)
@@ -299,7 +299,7 @@ function recurring(dir, cli) {
299
299
  const promote = cli.value('--promote')
300
300
  if (promote) {
301
301
  const one = state.find((candidate) => candidate.id === promote)
302
- if (!one) return fail(`${RC.FILE} no declara ${promote}`, 2)
302
+ if (!one) return fail(`${RC.FILE} no declara ${promote}`, USAGE)
303
303
  // La línea sale sola por stdout para que se pueda pegar o redirigir sin recortar nada; el destino,
304
304
  // que es lo único que falta decidir, va por stderr.
305
305
  console.error(`Pegala en el hito que corresponda de BACKLOG.md:`)
@@ -30,7 +30,7 @@ const CP = require('../config/paths')
30
30
  const AG = require('../agents/catalog')
31
31
  const RL = require('../automation/rules')
32
32
  const CT = require('./contract')
33
- const { fail, planningRoot, TODAY } = require('./io')
33
+ const { fail, planningRoot, TODAY, REFUSED } = require('./io')
34
34
 
35
35
  function check(dir, cli) {
36
36
  const root = planningRoot(dir)
@@ -197,7 +197,7 @@ function check(dir, cli) {
197
197
 
198
198
  for (const warning of warnings) console.warn(`⚠ ${warning}`)
199
199
  for (const error of errors) console.error(`✗ ${error}`)
200
- if (errors.length) fail(`\n${errors.length} error(es), ${warnings.length} advertencia(s)`)
200
+ if (errors.length) fail(`\n${errors.length} error(es), ${warnings.length} advertencia(s)`, REFUSED)
201
201
  console.log(
202
202
  `✓ planning válido: ${epics.length} épica(s), ${backlog.length} tarea(s) en cola, ` +
203
203
  `${done.entries.length} terminada(s)`,
@@ -14,7 +14,7 @@ const SC = require('../core/scan')
14
14
  const OB = require('../core/onboarding')
15
15
  const IN = require('./instance')
16
16
  const ST = require('../planning/state')
17
- const { fail, opsRoot } = require('./io')
17
+ const { fail, opsRoot, USAGE, REFUSED } = require('./io')
18
18
 
19
19
  // Cuántos servicios se listan en pantalla antes de recortar. El resto sigue en `--json`, que es lo que
20
20
  // consume el recorrido de arranque: recortar la lista es para leerla, no para acotar lo que se sabe.
@@ -45,7 +45,7 @@ const INTEGRATION = {
45
45
  && fs.existsSync(path.join(root, 'integrations', provider))
46
46
  if (!fromCauce && !own) {
47
47
  fail(`Cauce no trae un adaptador para ${provider}. Uno propio vive en integrations/${provider}/ y se `
48
- + `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`, 2)
48
+ + `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`, USAGE)
49
49
  }
50
50
  // Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
51
51
  // trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
@@ -86,7 +86,7 @@ const INTEGRATION = {
86
86
  const result = I.validate(root, provider || '')
87
87
  for (const warning of result.warnings) console.warn(`⚠ ${warning}`)
88
88
  for (const error of result.errors) console.error(`✗ ${error}`)
89
- if (result.errors.length) fail(`${result.errors.length} error(es) de integración`)
89
+ if (result.errors.length) fail(`${result.errors.length} error(es) de integración`, REFUSED)
90
90
  console.log(`✓ integraciones válidas${provider ? `: ${provider}` : ''}`)
91
91
  },
92
92
  },
@@ -203,14 +203,14 @@ function onboard(rootArg, cli, runner = '') {
203
203
  function providerRegistry(root) {
204
204
  const file = path.join(root, 'integrations', 'config.json')
205
205
  try { return { file, config: JSON.parse(fs.readFileSync(file, 'utf8')) } } catch (error) {
206
- return fail(`integrations/config.json ilegible: ${error.message}`)
206
+ return fail(`integrations/config.json ilegible: ${error.message}`, USAGE)
207
207
  }
208
208
  }
209
209
 
210
210
  function switchProvider(root, provider, enabled) {
211
211
  const { file, config } = providerRegistry(root)
212
212
  if (!config.providers || !config.providers[provider]) {
213
- fail(`${provider} no está en integrations/config.json.`)
213
+ fail(`${provider} no está en integrations/config.json.`, USAGE)
214
214
  }
215
215
  config.providers[provider].enabled = enabled
216
216
  F.atomicWriteJson(file, config)
@@ -218,8 +218,8 @@ function switchProvider(root, provider, enabled) {
218
218
 
219
219
  async function integration(action, rootArg, provider, key, cli) {
220
220
  const step = INTEGRATION[action]
221
- if (!step) fail(`Acción de integración desconocida: ${action || '(vacía)'}`, 2)
222
- if (step.missing && (!provider || (step.needsKey && !key))) fail(step.missing, 2)
221
+ if (!step) fail(`Acción de integración desconocida: ${action || '(vacía)'}`, USAGE)
222
+ if (step.missing && (!provider || (step.needsKey && !key))) fail(step.missing, USAGE)
223
223
  await step.run(path.resolve(rootArg || '.'), provider, key, cli)
224
224
  }
225
225
 
@@ -245,7 +245,7 @@ function automation(action, rootArg, runnerName, cli) {
245
245
  if (action === 'check') {
246
246
  const errors = A.check(root)
247
247
  for (const error of errors) console.error(`✗ ${error}`)
248
- if (errors.length) fail(`${errors.length} error(es) de automatización`)
248
+ if (errors.length) fail(`${errors.length} error(es) de automatización`, REFUSED)
249
249
  console.log(
250
250
  `✓ automatización válida: ${A.GUARD_NAMES.length} guards, ${A.RUNNER_NAMES.length} adaptadores`,
251
251
  )
@@ -260,22 +260,22 @@ function automation(action, rootArg, runnerName, cli) {
260
260
  }
261
261
  if (action === 'doctor') {
262
262
  let result
263
- try { result = A.doctor(root, runnerName) } catch (error) { fail(error.message, 2) }
263
+ try { result = A.doctor(root, runnerName) } catch (error) { fail(error.message, USAGE) }
264
264
  if (result.errors.length) {
265
- fail(`${runnerName}: ${result.errors.length} error(es), ${result.warnings.length} advertencia(s)`)
265
+ fail(`${runnerName}: ${result.errors.length} error(es), ${result.warnings.length} advertencia(s)`, REFUSED)
266
266
  }
267
267
  console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
268
268
  return
269
269
  }
270
270
  if (action === 'uninstall') {
271
- try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message, 2) }
271
+ try { A.uninstall(root, runnerName, console) } catch (error) { fail(error.message, USAGE) }
272
272
  console.log(' la instancia sigue en pie: borrar la carpeta ops es una decisión aparte.')
273
273
  return
274
274
  }
275
275
  if (action === 'install') {
276
276
  let runner
277
277
  const force = cli.has('--force')
278
- try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, 2) }
278
+ try { runner = A.install(root, runnerName, console, { force }) } catch (error) { fail(error.message, USAGE) }
279
279
  if (runnerName === 'codex') {
280
280
  console.log(' Codex deja los hooks nuevos sin correr hasta que los confíes: abrí una sesión')
281
281
  console.log(' y usá /hooks para revisarlos y marcarlos como confiables.')
@@ -284,20 +284,20 @@ function automation(action, rootArg, runnerName, cli) {
284
284
  console.log(` ${runnerName} no expone hooks nativos; aplica guards como prechecks.`)
285
285
  }
286
286
  const result = A.doctor(root, runnerName)
287
- if (result.errors.length) fail(`${runnerName}: instalación incompleta`)
287
+ if (result.errors.length) fail(`${runnerName}: instalación incompleta`, REFUSED)
288
288
  console.log(`✓ ${runnerName}: adaptador operativo (${result.warnings.length} advertencia(s))`)
289
289
  return
290
290
  }
291
- fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
291
+ fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, USAGE)
292
292
  }
293
293
 
294
294
  function secrets(action, rootArg) {
295
- if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`, 2)
295
+ if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`, USAGE)
296
296
  const result = SE.check(opsRoot(rootArg))
297
297
  if (!result.declared) return console.log(`Sin ${SE.DECLARATION}: no hay contrato de secretos que comprobar.`)
298
298
  for (const warning of result.warnings) console.log(`⚠ ${warning}`)
299
299
  for (const error of result.errors) console.error(`✗ ${error}`)
300
- if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos`)
300
+ if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos`, REFUSED)
301
301
  console.log(`✓ contrato de secretos: ${result.current} servicio(s) al día`)
302
302
  }
303
303
 
@@ -15,7 +15,7 @@ const ST = require('../planning/state')
15
15
  const CL = require('../planning/claims')
16
16
  const R = require('../core/repos')
17
17
  const O = require('../core/ownership')
18
- const { fail, planningRoot } = require('./io')
18
+ const { fail, planningRoot, USAGE, REFUSED } = require('./io')
19
19
 
20
20
  const git = (cwd, ...args) => spawnSync('git', args, { cwd, encoding: 'utf8' })
21
21
 
@@ -34,38 +34,38 @@ function existing(repo, branch) {
34
34
 
35
35
  function worktree(dir, slug, cli) {
36
36
  const root = planningRoot(dir)
37
- if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`', 2)
37
+ if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`', USAGE)
38
38
  const state = ST.snapshot(root)
39
39
  const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
40
- if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`, 2)
40
+ if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`, USAGE)
41
41
 
42
42
  // No reserva —eso es `claim`— pero se niega a montar sobre lo de otro: preparar un árbol para una
43
43
  // tarea ajena es trabajo que se va a tirar, y el aviso cuesta menos que descubrirlo después.
44
44
  const taken = state.claims.find((one) => one.slug === slug)
45
45
  if (taken && taken.runner !== CL.runner()) {
46
- return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie.`)
46
+ return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie.`, REFUSED)
47
47
  }
48
48
 
49
49
  const candidatos = R.reposFor(path.join(root, '..'), task.service)
50
50
  if (candidatos.length > 1) {
51
51
  return fail(`${task.service || '.'} existe en más de un repositorio (${candidatos.join(', ')}), así que `
52
- + 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.', 2)
52
+ + 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.', USAGE)
53
53
  }
54
54
  const repo = candidatos[0]
55
55
  if (!repo) {
56
56
  return fail(`no encontré el repositorio de ${task.service || '(sin service)'}: revisá workspaceRoots `
57
- + 'en ops.config.json y que la ruta del servicio exista.', 2)
57
+ + 'en ops.config.json y que la ruta del servicio exista.', USAGE)
58
58
  }
59
59
 
60
60
  const branch = CL.branchOf(slug)
61
61
  const already = existing(repo, branch)
62
62
  const target = already || path.join(path.dirname(repo), `${path.basename(repo)}-${slug}`)
63
63
  if (!already) {
64
- if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama.`)
64
+ if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama.`, REFUSED)
65
65
  const hasBranch = git(repo, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`).status === 0
66
66
  const args = hasBranch ? ['worktree', 'add', target, branch] : ['worktree', 'add', '-b', branch, target]
67
67
  const added = git(repo, ...args)
68
- if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}`)
68
+ if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}`, REFUSED)
69
69
  }
70
70
 
71
71
  if (cli.has('--json')) {
@@ -164,9 +164,16 @@ function engineAt(root, relative = '') {
164
164
  // de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
165
165
  // proyecto deba crear. Una sola implementación para las dos, o divergen.
166
166
  function packageDir(root, name) {
167
+ // La raíz declarada va también acá, y no sólo en `packagePath`. Las dos contestan dónde está el paquete
168
+ // sobre la misma instancia y desde el 158 contestaban distinto: en el layout documentado —`npm install`
169
+ // en la carpeta de la empresa y la instancia adentro— el motor se resolvía y el catálogo no, así que
170
+ // `check` pasaba en verde y `agents list` devolvía una lista vacía con exit 0, indistinguible de «esta
171
+ // instancia no tiene cargos». `evaluate` era peor: culpaba a un `SKILL.md` que falta (caso 171).
172
+ const above = declaredRoot(root)
167
173
  const candidates = [
168
174
  path.join(root, 'node_modules', '@ingeniomaps', 'cauce', name),
169
175
  path.join(root, name),
176
+ ...(above ? [path.join(above, 'node_modules', '@ingeniomaps', 'cauce', name)] : []),
170
177
  ]
171
178
  return candidates.find((dir) => fs.existsSync(path.join(dir, 'system'))
172
179
  || fs.existsSync(path.join(dir, 'roles', 'system'))) || ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingeniomaps/cauce",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "description": "Sistema portable de planificación y ejecución verificable para cualquier proyecto",
5
5
  "keywords": [
6
6
  "planning",