@ingeniomaps/cauce 0.94.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +238 -0
  2. package/automatization/workflows/autobuild.js +28 -1
  3. package/engine/agents/evaluations.js +20 -5
  4. package/engine/agents/fork.js +21 -8
  5. package/engine/agents/learning-sources.js +7 -7
  6. package/engine/agents/learning.js +15 -6
  7. package/engine/automation/check.js +8 -2
  8. package/engine/automation/config.js +9 -9
  9. package/engine/automation/index.js +19 -8
  10. package/engine/cli/archive.js +3 -6
  11. package/engine/cli/args.js +16 -2
  12. package/engine/cli/bench.js +9 -9
  13. package/engine/cli/catalog.js +36 -17
  14. package/engine/cli/claims.js +30 -32
  15. package/engine/cli/contract.js +29 -11
  16. package/engine/cli/dependency.js +2 -2
  17. package/engine/cli/instance.js +25 -24
  18. package/engine/cli/io.js +26 -4
  19. package/engine/cli/ops.js +24 -15
  20. package/engine/cli/planning.js +8 -8
  21. package/engine/cli/upgrade-report.js +10 -10
  22. package/engine/cli/validate.js +9 -9
  23. package/engine/cli/wiring.js +16 -16
  24. package/engine/cli/worktree.js +8 -8
  25. package/engine/core/onboarding.js +4 -1
  26. package/engine/core/ownership.js +9 -2
  27. package/engine/core/repos.js +18 -12
  28. package/engine/hooks/approval.js +9 -1
  29. package/engine/hooks/chat.js +27 -5
  30. package/engine/hooks/files.js +6 -6
  31. package/engine/hooks/input.js +5 -2
  32. package/engine/hooks/run.js +3 -3
  33. package/engine/hooks/shell.js +20 -6
  34. package/engine/hooks/verify.js +7 -7
  35. package/engine/integrations/proposals.js +5 -1
  36. package/engine/integrations/registry.js +7 -0
  37. package/engine/integrations/state.js +6 -3
  38. package/engine/planning/adoption.js +2 -2
  39. package/engine/planning/claims.js +8 -8
  40. package/engine/planning/parser.js +8 -8
  41. package/engine/planning/state.js +2 -2
  42. package/engine/planning/structure.js +7 -7
  43. package/package.json +1 -1
@@ -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 } = 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)
@@ -98,7 +98,7 @@ function scaffold(root, { name, mode, force = false, quiet = false }) {
98
98
  '{{PROJECT_NAME}}': name,
99
99
  '{{MODE}}': mode,
100
100
  '{{WORKSPACE_PATH}}': mode === 'embedded' ? '.' : '..',
101
- ...RC.sinceValues(new Date().toISOString().slice(0, 10)),
101
+ ...RC.sinceValues(TODAY()),
102
102
  }, force, providerNames(), quiet)
103
103
  // No se copia `.github/`: `ci.yml` valida el toolkit con `npm run ci` —que una instancia no tiene— y
104
104
  // el ciclo de aprendizaje dejó de distribuirse en 0.4.0. Copiar salteando lo que no aplica dejaba
@@ -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
 
@@ -309,17 +310,17 @@ function upgrade(dir, cli) {
309
310
  //
310
311
  // El aviso sale en **cada** corrida y no sólo la primera: una instancia con medio molde congelado y
311
312
  // sin enterarse es el otro modo de fallo, y es silencioso.
312
- const conservados = force ? new Set() : new Set(changed)
313
- if (conservados.size) {
314
- for (const file of conservados) console.log(`= conservado ${file} (editado localmente)`)
315
- console.log(`\n${conservados.size} archivo(s) del molde quedan congelados por tu edición.`)
316
- console.log(`${adviceFor([...conservados])}\n`)
313
+ const keptFiles = force ? new Set() : new Set(changed)
314
+ if (keptFiles.size) {
315
+ for (const file of keptFiles) console.log(`= conservado ${file} (editado localmente)`)
316
+ console.log(`\n${keptFiles.size} archivo(s) del molde quedan congelados por tu edición.`)
317
+ console.log(`${adviceFor([...keptFiles])}\n`)
317
318
  console.log('Para tomar la versión nueva y descartar la tuya, repetí con --force.\n')
318
319
  }
319
320
  // Un archivo propio que se llama como uno que el paquete empieza a traer (caso 110): se conserva y se
320
321
  // dice, igual que una edición local, y `--force` lo reemplaza diciéndolo.
321
- const choques = new Set(force ? [] : colliding)
322
- for (const file of choques) {
322
+ const collisions = new Set(force ? [] : colliding)
323
+ for (const file of collisions) {
323
324
  console.log(`= conservado ${file}: ya existía y Cauce no lo entregó, así que el del paquete no se `
324
325
  + 'instaló. Renombrá el tuyo y repetí, o repetí con --force para reemplazarlo.')
325
326
  }
@@ -345,7 +346,7 @@ function upgrade(dir, cli) {
345
346
  '{{PROJECT_NAME}}': config.project || path.basename(root),
346
347
  '{{MODE}}': O.mode(root),
347
348
  '{{WORKSPACE_PATH}}': O.mode(root) === 'embedded' ? '.' : '..',
348
- ...RC.sinceValues(new Date().toISOString().slice(0, 10)),
349
+ ...RC.sinceValues(TODAY()),
349
350
  })) content = content.replaceAll(key, value)
350
351
  F.atomicWrite(target, content)
351
352
  added.push(relative)
@@ -368,13 +369,13 @@ function upgrade(dir, cli) {
368
369
 
369
370
  const conservar = (file) => {
370
371
  const relative = path.relative(root, file).replace(/\\/g, '/')
371
- return conservados.has(relative) || choques.has(relative)
372
+ return keptFiles.has(relative) || collisions.has(relative)
372
373
  }
373
374
  for (const relative of [...system, ...O.RUNTIME_PATHS]) {
374
375
  const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
375
376
  if (!fs.existsSync(origin)) continue
376
377
  const target = path.join(root, relative)
377
- if (conservados.has(relative)) continue
378
+ if (keptFiles.has(relative)) continue
378
379
  // Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
379
380
  // o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
380
381
  if (fs.statSync(origin).isDirectory()) copyRuntime(origin, target, false, root, [], conservar)
@@ -389,7 +390,7 @@ function upgrade(dir, cli) {
389
390
 
390
391
  // Retirar lo que el toolkit ya no distribuye, después de haber actualizado lo que sí.
391
392
  const retired = []
392
- const pendientes = []
393
+ const pending = []
393
394
  for (const relative of O.RETIRED) {
394
395
  const target = path.join(root, relative)
395
396
  if (!fs.existsSync(target)) continue
@@ -398,10 +399,10 @@ function upgrade(dir, cli) {
398
399
  // también usa para lo suyo, borrar el directorio entero se llevaba puesto contenido que nadie
399
400
  // había entregado —un `autobuild.js` propio, los workflows de una empresa— sin confirmación y sin
400
401
  // vuelta atrás. Se conserva y se nombra; `--force` es la salida, igual que para una edición local.
401
- const contenido = O.RETIRED_COMPARTIDO.includes(relative) && fs.statSync(target).isDirectory()
402
+ const leftover = O.RETIRED_SHARED.includes(relative) && fs.statSync(target).isDirectory()
402
403
  ? O.treeFiles(target)
403
404
  : []
404
- if (contenido.length && !force) { pendientes.push({ relative, files: contenido }); continue }
405
+ if (leftover.length && !force) { pending.push({ relative, files: leftover }); continue }
405
406
  fs.rmSync(target, { recursive: true, force: true })
406
407
  retired.push(relative)
407
408
  }
@@ -413,7 +414,7 @@ function upgrade(dir, cli) {
413
414
  // conservar **ese** digest: registrar el de disco lo volvería idéntico a lo entregado, dejaría de
414
415
  // detectarse como editado y la corrida siguiente lo pisaría sin decir nada. Es el 001 de vuelta por
415
416
  // la puerta de atrás, y no se ve mirando el archivo — se ve dos upgrades después.
416
- const entregado = { ...record }
417
+ const delivered = { ...record }
417
418
  for (const relative of O.trackedPaths()) {
418
419
  if (fs.existsSync(path.join(root, relative))) {
419
420
  record = M.record(root, relative, O.deliveredFiles(root, relative), record)
@@ -428,8 +429,8 @@ function upgrade(dir, cli) {
428
429
  }
429
430
  }
430
431
  record = M.recordPaths(root, O.SYSTEM_FILES, record)
431
- for (const file of conservados) if (entregado[file]) record[file] = entregado[file]
432
- for (const file of choques) delete record[file]
432
+ for (const file of keptFiles) if (delivered[file]) record[file] = delivered[file]
433
+ for (const file of collisions) delete record[file]
433
434
  // El registro de forks se poda igual que el de archivos: un cargo devuelto al catálogo deja su
434
435
  // entrada, y una entrada sin copia sólo puede producir avisos sobre algo que no está.
435
436
  const kept = Object.fromEntries(Object.entries(M.readForks(root)).filter(
@@ -447,7 +448,7 @@ function upgrade(dir, cli) {
447
448
  // que no ocurrieron (caso 048).
448
449
  reportUpgrade({
449
450
  root, from, to, system, retired, added, overrides, pinned, droppedBlocks,
450
- descartados: force ? changed : [], conservados: [...conservados], pendientes,
451
+ discarded: force ? changed : [], keptFiles: [...keptFiles], pending,
451
452
  })
452
453
  }
453
454
 
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
@@ -195,15 +200,19 @@ const NO_FLAGS = { has: () => false, value: (_flag, fallback = '') => fallback }
195
200
 
196
201
  async function run(cli) {
197
202
  const [command] = cli.positional
198
- if (!command || ['help', '--help', '-h'].includes(command)) return usage()
199
- if (!FLAGS[command]) { usage(); fail(`Comando desconocido: ${command}`, 2) }
203
+ // `--help` y `-h` nunca llegan acá: `parse` los reconoce como banderas, así que el posicional queda
204
+ // vacío y lo atiende el `!command` de al lado. Lo que sí es un posicional es `help` a secas.
205
+ if (!command || command === 'help') return usage()
206
+ // `Object.hasOwn` y no `FLAGS[command]`: `constructor` heredado de `Object.prototype` pasaba por
207
+ // comando válido y el CLI salía con 0 sin hacer nada.
208
+ if (!Object.hasOwn(FLAGS, command)) { usage(); fail(`Comando desconocido: ${command}`, USAGE) }
200
209
  // `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
201
210
  // actual en vez de explicarse.
202
- if (cli.has('--help') || cli.has('-h')) return usage()
211
+ if (cli.has('--help')) return usage()
203
212
  const unknown = cli.unknown(command)
204
213
  if (unknown.length) {
205
214
  const accepts = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
206
- fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`, 2)
215
+ fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`, USAGE)
207
216
  }
208
217
  const arg = cli.positional
209
218
  if (command === 'init') await init(arg[1], cli)
@@ -232,7 +241,7 @@ async function run(cli) {
232
241
  else if (command === 'automation') W.automation(arg[1], arg[2], arg[3], cli)
233
242
  else if (command === 'learn') CAT.learn(arg[1], cli)
234
243
  else if (command === 'evaluate') CAT.evaluate(arg[1], arg[2], cli)
235
- 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)
236
245
  }
237
246
 
238
- 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 */ }
@@ -141,16 +141,16 @@ function context(dir, cli) {
141
141
  // Un filtro elige dónde buscar trabajo **nuevo**; no puede esconder el que ya tenés. Sin esto, pedir
142
142
  // otro hito mientras sostenías una tarea ofrecía una segunda que `claim` después se niega a dar: el
143
143
  // comando que dice qué hacer y el que lo autoriza contestaban distinto, y sólo se veía al reclamar.
144
- const propio = state.claims.find((one) => one.runner === from && !state.done.set.has(one.slug))
144
+ const own = state.claims.find((one) => one.runner === from && !state.done.set.has(one.slug))
145
145
  let hitoOmitido = ''
146
146
  if (hito) {
147
147
  const existe = state.milestones.some((one) => one.slug === hito)
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
- if (propio) hitoOmitido = `${hito} no se aplica: ya tenés ${propio.slug} tomada`
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)
155
155
  }
156
156
  const gate = path.join(root, 'AWAITING_REVIEW.md')
@@ -222,13 +222,13 @@ function context(dir, cli) {
222
222
  // Es el comando que existe para decir qué toca ahora, contestando «nada» cuando lo que toca es eso.
223
223
  // Tu propio nombre en una tarea «ajena» es la señal de que sos vos desde otro runner, y sin decirlo se
224
224
  // lee como que alguien te ganó la tarea.
225
- const dueño = (one) => (one.owner === report.owner ? `${one.owner} — vos, desde otro runner` : one.owner)
225
+ const ownerLabel = (one) => (one.owner === report.owner ? `${one.owner} — vos, desde otro runner` : one.owner)
226
226
  // Que el reclamo sea tuyo desde otro id ya se decía; que además haya un **plan escrito** bajo ese id, no.
227
227
  // Esa es la mitad que cuesta la sesión: los pasos ya hechos están en un archivo que nadie nombra, y el id
228
228
  // que lo recupera es justo el que esta sesión no supo deducir. Los dos datos están en el reclamo.
229
229
  const tomadas = () => {
230
230
  for (const one of report.taken) {
231
- console.log(`TAKEN ${one.slug} (${dueño(one)})`)
231
+ console.log(`TAKEN ${one.slug} (${ownerLabel(one)})`)
232
232
  if (one.owner === report.owner && one.wip) {
233
233
  console.log(`PLAN ${one.slug}: su plan está en wip/${one.wip} — retomalo con `
234
234
  + `\`export CAUCE_RUNNER=${one.runner}\``)
@@ -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:`)
@@ -26,7 +26,7 @@ function adviceFor(changed) {
26
26
  const advice = []
27
27
  if (ruleFiles.length) {
28
28
  advice.push(
29
- 'Las ruleFiles y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
29
+ 'Las reglas y decisiones bajo system/ son del toolkit. Para cambiar una, escribí la tuya al\n'
30
30
  + 'lado con el mismo ID: el proyecto manda y `check` lo reporta como override explícito.',
31
31
  )
32
32
  }
@@ -47,11 +47,11 @@ function adviceFor(changed) {
47
47
  // El consejo de arriba manda mudar lo propio a donde sí es del proyecto, y para estos cuatro no hay
48
48
  // adónde: no existe un PROTOCOL de la empresa que le gane al del toolkit como sí lo hay en `rules/`.
49
49
  // Sin decirlo, quien adoptó Cauce sobre su propio proceso lee un consejo que no puede seguir.
50
- const SIN_CONTRAPARTE = ['planning/PROTOCOL.md', 'planning/METHODOLOGY.md', 'planning/FLOW.md', 'Makefile']
51
- const propios = docs.filter((file) => SIN_CONTRAPARTE.includes(file))
52
- if (propios.length) {
50
+ const NO_COUNTERPART = ['planning/PROTOCOL.md', 'planning/METHODOLOGY.md', 'planning/FLOW.md', 'Makefile']
51
+ const ownDocs = docs.filter((file) => NO_COUNTERPART.includes(file))
52
+ if (ownDocs.length) {
53
53
  advice.push(
54
- `${propios.join(', ')} no tienen contraparte propia adónde mudarse: son del toolkit y no hay\n`
54
+ `${ownDocs.join(', ')} no tienen contraparte propia adónde mudarse: son del toolkit y no hay\n`
55
55
  + 'una versión del proyecto que le gane. Quedan congelados con tu versión y el resto se actualiza\n'
56
56
  + 'igual. Adoptar el del toolkit es trabajo propio —comparar los dos procesos y decidir—, no un flag.',
57
57
  )
@@ -74,18 +74,18 @@ function adviceFor(changed) {
74
74
  // entero en vez de recalcular nada: lo que se informa es exactamente lo que ocurrió.
75
75
  function reportUpgrade({
76
76
  root, from, to, system, retired, added, overrides, pinned, droppedBlocks,
77
- descartados, conservados, pendientes,
77
+ discarded, keptFiles, pending,
78
78
  }) {
79
79
  console.log(`✓ Cauce ${from || '(previa)'} → ${to}`)
80
80
  // Descartar con --force es legítimo; hacerlo sin dejar rastro no. Queda en la salida del comando,
81
81
  // que es la evidencia que el protocolo pide para cualquier cambio. Y lo que se conservó no se vuelve
82
82
  // a enumerar acá: ya salió con su consejo antes de escribir nada, y repetirlo con el glifo del
83
83
  // descarte es lo que volvía ilegible el bloque.
84
- for (const file of descartados) console.log(`− descartado tu cambio en ${file}`)
85
- if (conservados.length) console.log(`= ${conservados.length} archivo(s) conservados por tu edición`)
84
+ for (const file of discarded) console.log(`− descartado tu cambio en ${file}`)
85
+ if (keptFiles.length) console.log(`= ${keptFiles.length} archivo(s) conservados por tu edición`)
86
86
  for (const relative of retired) console.log(`− retirado ${relative}: Cauce ya no lo distribuye`)
87
87
  // Sin glifo de acción porque no hubo ninguna: la ruta sigue ahí y el contenido también.
88
- for (const { relative, files } of pendientes) {
88
+ for (const { relative, files } of pending) {
89
89
  console.log(` ${relative}: ${files.length} archivo(s) que Cauce no entregó; la ruta se retiró y `
90
90
  + 'el contenido queda. Movelo adonde lo quieras y borrala, o repetí con --force.')
91
91
  }
@@ -103,7 +103,7 @@ function reportUpgrade({
103
103
  }
104
104
  // Sólo cuando es cierto, y ahora lo decide lo que pasó y no una condición: se dice justo cuando no
105
105
  // se descartó nada, que incluye la corrida que conservó veinte archivos.
106
- if (!descartados.length) console.log(' planning, organization y todo lo propio quedaron intactos')
106
+ if (!discarded.length) console.log(' planning, organization y todo lo propio quedaron intactos')
107
107
  // No se borra: sin la dependencia declarada, quitarle `.ops/` la dejaría sin motor. Se avisa y
108
108
  // decide una persona.
109
109
  if (fs.existsSync(path.join(root, '.ops', 'engine'))) {
@@ -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)
@@ -137,19 +137,19 @@ function check(dir, cli) {
137
137
  // Lo que `upgrade` conserva por estar editado deja de recibir mejoras, y eso es una deuda que no
138
138
  // avisa sola: la instancia queda con medio molde viejo y todo se ve normal. Sale acá para que se vea
139
139
  // en cada corrida y no sólo el día que alguien actualiza.
140
- const congelados = O.localChanges(path.resolve(root, '..'))
141
- if (congelados.length) {
142
- warnings.push(`${congelados.length} archivo(s) del molde congelados por edición local; `
140
+ const frozen = O.localChanges(path.resolve(root, '..'))
141
+ if (frozen.length) {
142
+ warnings.push(`${frozen.length} archivo(s) del molde congelados por edición local; `
143
143
  + '`upgrade` los conserva y no les trae mejoras')
144
144
  }
145
145
 
146
146
  // Y lo que `upgrade` no retiró porque no pudo demostrar que fuera suyo: queda ahí, sin colgar de
147
147
  // ningún mecanismo, hasta que alguien lo mueva o lo borre. Se cuenta por lo mismo que los congelados
148
148
  // — un resto que no se ve se vuelve permanente.
149
- const restos = O.RETIRED_COMPARTIDO.filter((relative) => fs.existsSync(path.join(root, '..', relative)))
150
- if (restos.length) {
151
- warnings.push(`${restos.length} ruta(s) retiradas siguen en disco con contenido tuyo `
152
- + `(${restos.join(', ')}); Cauce ya no las distribuye ni las toca`)
149
+ const leftovers = O.RETIRED_SHARED.filter((relative) => fs.existsSync(path.join(root, '..', relative)))
150
+ if (leftovers.length) {
151
+ warnings.push(`${leftovers.length} ruta(s) retiradas siguen en disco con contenido tuyo `
152
+ + `(${leftovers.join(', ')}); Cauce ya no las distribuye ni las toca`)
153
153
  }
154
154
 
155
155
  const integration = I.validate(path.resolve(root, '..'))
@@ -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