@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.
- package/CHANGELOG.md +238 -0
- package/automatization/workflows/autobuild.js +28 -1
- package/engine/agents/evaluations.js +20 -5
- package/engine/agents/fork.js +21 -8
- package/engine/agents/learning-sources.js +7 -7
- package/engine/agents/learning.js +15 -6
- package/engine/automation/check.js +8 -2
- package/engine/automation/config.js +9 -9
- package/engine/automation/index.js +19 -8
- package/engine/cli/archive.js +3 -6
- package/engine/cli/args.js +16 -2
- package/engine/cli/bench.js +9 -9
- package/engine/cli/catalog.js +36 -17
- package/engine/cli/claims.js +30 -32
- package/engine/cli/contract.js +29 -11
- package/engine/cli/dependency.js +2 -2
- package/engine/cli/instance.js +25 -24
- package/engine/cli/io.js +26 -4
- package/engine/cli/ops.js +24 -15
- package/engine/cli/planning.js +8 -8
- package/engine/cli/upgrade-report.js +10 -10
- package/engine/cli/validate.js +9 -9
- package/engine/cli/wiring.js +16 -16
- package/engine/cli/worktree.js +8 -8
- package/engine/core/onboarding.js +4 -1
- package/engine/core/ownership.js +9 -2
- package/engine/core/repos.js +18 -12
- package/engine/hooks/approval.js +9 -1
- package/engine/hooks/chat.js +27 -5
- package/engine/hooks/files.js +6 -6
- package/engine/hooks/input.js +5 -2
- package/engine/hooks/run.js +3 -3
- package/engine/hooks/shell.js +20 -6
- package/engine/hooks/verify.js +7 -7
- package/engine/integrations/proposals.js +5 -1
- package/engine/integrations/registry.js +7 -0
- package/engine/integrations/state.js +6 -3
- package/engine/planning/adoption.js +2 -2
- package/engine/planning/claims.js +8 -8
- package/engine/planning/parser.js +8 -8
- package/engine/planning/state.js +2 -2
- package/engine/planning/structure.js +7 -7
- package/package.json +1 -1
package/engine/cli/instance.js
CHANGED
|
@@ -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(
|
|
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.`,
|
|
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.`,
|
|
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.`,
|
|
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.`,
|
|
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
|
|
313
|
-
if (
|
|
314
|
-
for (const file of
|
|
315
|
-
console.log(`\n${
|
|
316
|
-
console.log(`${adviceFor([...
|
|
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
|
|
322
|
-
for (const file of
|
|
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(
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
402
|
+
const leftover = O.RETIRED_SHARED.includes(relative) && fs.statSync(target).isDirectory()
|
|
402
403
|
? O.treeFiles(target)
|
|
403
404
|
: []
|
|
404
|
-
if (
|
|
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
|
|
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
|
|
432
|
-
for (const file of
|
|
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
|
-
|
|
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
|
-
|
|
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.`,
|
|
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.`,
|
|
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.',
|
|
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,
|
|
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,
|
|
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
|
-
|
|
199
|
-
|
|
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')
|
|
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}`,
|
|
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))
|
package/engine/cli/planning.js
CHANGED
|
@@ -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',
|
|
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
|
|
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}`,
|
|
151
|
+
return fail(`el hito ${hito} no existe. Hay: ${hay}`, USAGE)
|
|
152
152
|
}
|
|
153
|
-
if (
|
|
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
|
|
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} (${
|
|
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}`,
|
|
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
|
|
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
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
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
|
-
`${
|
|
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
|
-
|
|
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
|
|
85
|
-
if (
|
|
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
|
|
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 (!
|
|
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'))) {
|
package/engine/cli/validate.js
CHANGED
|
@@ -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
|
|
141
|
-
if (
|
|
142
|
-
warnings.push(`${
|
|
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
|
|
150
|
-
if (
|
|
151
|
-
warnings.push(`${
|
|
152
|
-
+ `(${
|
|
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)`,
|
package/engine/cli/wiring.js
CHANGED
|
@@ -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.`,
|
|
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)'}`,
|
|
222
|
-
if (step.missing && (!provider || (step.needsKey && !key))) fail(step.missing,
|
|
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,
|
|
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,
|
|
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,
|
|
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)'}`,
|
|
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)'}`,
|
|
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
|
|