@ingeniomaps/cauce 0.93.0 → 0.95.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 +311 -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 +2 -2
- package/engine/automation/config.js +9 -9
- package/engine/automation/index.js +19 -8
- package/engine/automation/rules.js +27 -9
- package/engine/cli/archive.js +1 -4
- package/engine/cli/args.js +16 -2
- package/engine/cli/bench.js +3 -3
- package/engine/cli/catalog.js +3 -3
- package/engine/cli/claims.js +21 -23
- package/engine/cli/contract.js +24 -7
- package/engine/cli/instance.js +19 -19
- package/engine/cli/ops.js +7 -3
- package/engine/cli/planning.js +4 -4
- package/engine/cli/upgrade-report.js +10 -10
- package/engine/cli/validate.js +7 -7
- package/engine/core/onboarding.js +4 -1
- package/engine/core/ownership.js +2 -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/template/planning/rules/README.md +8 -5
- package/template/planning/rules/system/code-shape.md +19 -0
- package/template/planning/rules/system/commits.md +35 -0
- package/template/planning/rules/system/conduct.md +23 -0
- package/template/planning/rules/system/process.md +48 -118
- package/template/planning/rules/system/runs.md +153 -0
|
@@ -45,9 +45,9 @@ function probeBridge(paths, runner) {
|
|
|
45
45
|
const problems = []
|
|
46
46
|
// Desde la raíz y desde una carpeta de adentro: si el runner lanza el hook con otro cwd, la ruta
|
|
47
47
|
// relativa de su configuración deja de resolver y eso hay que verlo acá, no en la primera sesión.
|
|
48
|
-
const
|
|
48
|
+
const launchDirs = [paths.install, path.dirname(script)]
|
|
49
49
|
for (const event of hookEvents) {
|
|
50
|
-
for (const cwd of
|
|
50
|
+
for (const cwd of launchDirs) {
|
|
51
51
|
const payload = JSON.stringify({ toolCall: { args: { CommandLine: 'ls', Cwd: paths.install } } })
|
|
52
52
|
const result = spawnSync(process.execPath, [script, event], { cwd, input: payload, encoding: 'utf8' })
|
|
53
53
|
let response = {}
|
|
@@ -75,11 +75,11 @@ function doctor(root, name, output = console) {
|
|
|
75
75
|
const warnings = []
|
|
76
76
|
try {
|
|
77
77
|
const expected = runnerConfig(paths, root)
|
|
78
|
-
const
|
|
79
|
-
if (!includesConfig(
|
|
78
|
+
const installed = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
79
|
+
if (!includesConfig(installed, expected)) {
|
|
80
80
|
errors.push(`${runner.config.target}: configuración instalada incompleta o divergente`)
|
|
81
81
|
}
|
|
82
|
-
const legacy = legacyGuardWiring(
|
|
82
|
+
const legacy = legacyGuardWiring(installed)
|
|
83
83
|
if (legacy.length) {
|
|
84
84
|
warnings.push(`${runner.config.target}: ${legacy.join(', ')} siguen registrados sueltos junto al grupo; `
|
|
85
85
|
+ 'cada uno se ejecuta dos veces. Borrá esas entradas del archivo para quedarte sólo con el grupo')
|
|
@@ -221,6 +221,18 @@ function uninstall(root, name, output = console) {
|
|
|
221
221
|
}
|
|
222
222
|
const runner = runnerManifest(root, name)
|
|
223
223
|
const paths = runnerPaths(root, name, runner)
|
|
224
|
+
// La configuración se lee **antes** de borrar nada, aunque se use al final. Leyéndola al final, un
|
|
225
|
+
// `settings.json` a medio fusionar hacía morir el comando con el mensaje crudo del parser —que no
|
|
226
|
+
// nombra ni un archivo— con los workflows y los cargos ya borrados: la instancia quedaba a medio
|
|
227
|
+
// desinstalar, con la configuración registrando guards que no existen. Acá no hay nada que deshacer
|
|
228
|
+
// porque todavía no se tocó el disco, que es más barato que cualquier rollback.
|
|
229
|
+
const hasConfig = fs.existsSync(paths.configTarget)
|
|
230
|
+
let config
|
|
231
|
+
if (hasConfig) {
|
|
232
|
+
try { config = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8')) } catch (error) {
|
|
233
|
+
throw new Error(`${runner.config.target} contiene JSON inválido (${error.message})`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
224
236
|
const prefix = opsPrefix(root)
|
|
225
237
|
const recorded = M.readRunners(root)
|
|
226
238
|
const kept = []
|
|
@@ -266,9 +278,8 @@ function uninstall(root, name, output = console) {
|
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
|
|
269
|
-
if (
|
|
270
|
-
const
|
|
271
|
-
const clean = unmergeConfig(unmergeConfig(current, runnerConfig(paths, root)), runner.config.retired || {})
|
|
281
|
+
if (hasConfig) {
|
|
282
|
+
const clean = unmergeConfig(unmergeConfig(config, runnerConfig(paths, root)), runner.config.retired || {})
|
|
272
283
|
if (clean && Object.keys(clean).length) F.atomicWriteJson(paths.configTarget, clean)
|
|
273
284
|
else { removeFile(paths.configTarget, paths.install); removed += 1 }
|
|
274
285
|
output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
|
|
@@ -67,38 +67,56 @@ function split(root) {
|
|
|
67
67
|
function weight(root) {
|
|
68
68
|
const { loaded } = split(root)
|
|
69
69
|
let bytes = 0
|
|
70
|
+
let own = 0
|
|
70
71
|
const files = []
|
|
71
72
|
for (const file of loaded) {
|
|
72
73
|
try {
|
|
73
74
|
const size = fs.statSync(path.join(root, file)).size
|
|
74
75
|
bytes += size
|
|
76
|
+
// Lo propio es lo que no vive en `rules/system/`, que es exactamente lo que el proyecto escribió:
|
|
77
|
+
// una regla del sistema que sobrescribió deja de cargarse y la suya ocupa su lugar, así que
|
|
78
|
+
// contarla como propia es correcto — la escribió él y la puede achicar.
|
|
79
|
+
if (!file.includes('/system/')) own += size
|
|
75
80
|
files.push({ file, size })
|
|
76
81
|
} catch { /* la que no está en disco ya la reporta `check` por su lado */ }
|
|
77
82
|
}
|
|
78
|
-
return { count: loaded.length, bytes, files: files.sort((a, b) => b.size - a.size) }
|
|
83
|
+
return { count: loaded.length, bytes, own, files: files.sort((a, b) => b.size - a.size) }
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
const KB = (bytes) => `${(bytes / 1024).toFixed(1)} KB`
|
|
82
87
|
|
|
83
|
-
// A partir de dónde el
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
+
// A partir de dónde lo que el proyecto agregó deja de ser el costo de arrancar y pasa a ser una decisión
|
|
89
|
+
// que conviene mirar. **Se compara contra lo propio y no contra el total**, y esa es la diferencia que
|
|
90
|
+
// hace al número significar algo.
|
|
91
|
+
//
|
|
92
|
+
// Contra el total, el piso del toolkit y las reglas de la empresa salían del mismo bolsillo: el aviso
|
|
93
|
+
// decía «tu bloque pesa» cuando la mitad la habíamos puesto nosotros, y cada regla que Cauce agregaba le
|
|
94
|
+
// achicaba el margen sin que nadie lo decidiera. El 64 tampoco salió de un costo medido: salió de
|
|
95
|
+
// esquivar nuestro propio piso —el caso 141 proponía 60 y se subió porque lo que Cauce ponía ya eran
|
|
96
|
+
// 61,9 KB—, así que había que reelegirlo cada vez que el toolkit enseñaba algo. Medido sobre lo propio,
|
|
97
|
+
// el número deja de depender de nosotros y no se toca cuando el piso crece.
|
|
98
|
+
//
|
|
99
|
+
// El valor sigue siendo el que había, y eso es a propósito: cambiar qué se mide y cuánto a la vez deja
|
|
100
|
+
// sin saber cuál de los dos movió el resultado. Lo que se sabe hoy es que **64 está por debajo de lo que
|
|
101
|
+
// una empresa real usa**: una instancia medida tiene 93,8 KB de reglas propias, así que el aviso le sale
|
|
102
|
+
// desde el día que instaló. Elegir el número con esa evidencia es una decisión aparte, y la cuenta que la
|
|
103
|
+
// habilita está en el 141: 61,9 KB ≈ 15,9 K tokens, o sea ~3,9 KB por 1K tokens en **cada** agente.
|
|
88
104
|
const HEAVY = 64 * 1024
|
|
89
105
|
|
|
90
106
|
// La línea que declara el peso, para que la digan igual `install` y `check`. Nombra las dos más grandes
|
|
91
107
|
// porque es lo accionable: saber que el bloque pesa no dice cuál conviene declarar por superficie.
|
|
92
108
|
function weightLine(root) {
|
|
93
|
-
const { count, bytes, files } = weight(root)
|
|
109
|
+
const { count, bytes, own, files } = weight(root)
|
|
94
110
|
const top = files.slice(0, 2).map((one) => path.basename(one.file)).join(', ')
|
|
111
|
+
// El total es lo que paga el agente y lo propio es lo único sobre lo que el proyecto puede hacer algo,
|
|
112
|
+
// así que van los dos: con uno solo, o el número no es el costo real o no es accionable.
|
|
95
113
|
return `el bloque de reglas carga ${count} archivo(s), ${KB(bytes)} en cada agente`
|
|
96
|
-
+ (top ? ` (las más grandes: ${top})` : ''
|
|
114
|
+
+ ` (${KB(own)} propias)${top ? ` (las más grandes: ${top})` : ''}`
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
// Sólo cuando pasó el umbral. Devuelve lista porque es lo que `check` empalma con el resto de avisos.
|
|
100
118
|
function heavyRules(root) {
|
|
101
|
-
return weight(root).
|
|
119
|
+
return weight(root).own > HEAVY ? [weightLine(root)] : []
|
|
102
120
|
}
|
|
103
121
|
|
|
104
122
|
// Sin raíz el marcador queda como está —así lo leen las pruebas que revisan el texto de un adaptador—: un
|
package/engine/cli/archive.js
CHANGED
|
@@ -11,10 +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 } = require('./io')
|
|
15
|
-
|
|
16
|
-
// La fecha de hoy, la misma que usan los comandos que leen.
|
|
17
|
-
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
14
|
+
const { fail, planningRoot, TODAY } = require('./io')
|
|
18
15
|
|
|
19
16
|
// El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
|
|
20
17
|
// a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
|
package/engine/cli/args.js
CHANGED
|
@@ -41,6 +41,12 @@ const FLAGS = {
|
|
|
41
41
|
flow: ['--json'],
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Qué banderas acepta un comando, y `[]` si el comando no existe. `FLAGS[nombre]` a secas resuelve
|
|
45
|
+
// contra `Object.prototype`, así que `constructor` contestaba una función —que es verdadera, y el CLI
|
|
46
|
+
// daba el comando por bueno— y `toString` contestaba un método sin `.includes`, que reventaba acá
|
|
47
|
+
// mismo. No es un nombre exótico: es lo que sale de pasarle a `ops` una variable que vino vacía.
|
|
48
|
+
const accepted = (command) => (Object.hasOwn(FLAGS, command) ? FLAGS[command] : [])
|
|
49
|
+
|
|
44
50
|
// La línea de comandos, leída una sola vez. Antes cada función buscaba sus banderas en
|
|
45
51
|
// `process.argv`, veinticinco veces y a cualquier profundidad: `evaluationBench` sacaba `--force` de
|
|
46
52
|
// ahí en vez de recibirlo, así que su firma no decía de qué dependía y probar un comando exigía
|
|
@@ -51,6 +57,10 @@ function parse(argv) {
|
|
|
51
57
|
const positional = []
|
|
52
58
|
for (let index = 0; index < argv.length; index += 1) {
|
|
53
59
|
const value = argv[index]
|
|
60
|
+
// `-h` es la única bandera corta que el CLI anuncia, y como no empieza con `--` caía de posicional:
|
|
61
|
+
// `ops check -h` tomaba `-h` por la raíz del planning y contestaba que ahí no había ninguno. Se
|
|
62
|
+
// normaliza a `--help` para que adentro haya una sola grafía y nadie tenga que preguntar por las dos.
|
|
63
|
+
if (value === '-h') { flags.add('--help'); continue }
|
|
54
64
|
if (!value.startsWith('--')) { positional.push(value); continue }
|
|
55
65
|
flags.add(value)
|
|
56
66
|
if (VALUED_FLAGS.has(value)) { values[value] = argv[index + 1] || ''; index += 1 }
|
|
@@ -60,8 +70,12 @@ function parse(argv) {
|
|
|
60
70
|
has: (flag) => flags.has(flag),
|
|
61
71
|
value: (flag, fallback = '') => values[flag] || fallback,
|
|
62
72
|
// Lo que el comando no declara en `FLAGS`. Se calcula sobre `flags` y no sobre argv crudo para
|
|
63
|
-
// que el valor de una bandera con valor no se confunda con una bandera suelta.
|
|
64
|
-
|
|
73
|
+
// que el valor de una bandera con valor no se confunda con una bandera suelta. `--help` la acepta
|
|
74
|
+
// cualquier comando y por eso no la declara ninguno: sin esta excepción, pedir ayuda sería una
|
|
75
|
+
// bandera desconocida.
|
|
76
|
+
unknown: (command) => [...flags]
|
|
77
|
+
.filter((flag) => flag !== '--help')
|
|
78
|
+
.filter((flag) => !accepted(command).includes(flag)),
|
|
65
79
|
}
|
|
66
80
|
}
|
|
67
81
|
|
package/engine/cli/bench.js
CHANGED
|
@@ -73,9 +73,9 @@ function benchSurvived(dir, since) {
|
|
|
73
73
|
// la rama se ejerce en milisegundos y sobre un temporal que la prueba acaba de crear.
|
|
74
74
|
function clearBench(dir, scratch, remove = fs.rmSync) {
|
|
75
75
|
const target = path.resolve(dir)
|
|
76
|
-
const
|
|
77
|
-
if (!target.startsWith(
|
|
78
|
-
return `no se borra ${target}: no cuelga de ${
|
|
76
|
+
const benchRoot = path.resolve(scratch)
|
|
77
|
+
if (!target.startsWith(benchRoot + path.sep)) {
|
|
78
|
+
return `no se borra ${target}: no cuelga de ${benchRoot}, así que no es un banco de evaluación.`
|
|
79
79
|
}
|
|
80
80
|
// El instante de arranque, para poder fechar lo que sobreviva: es lo único que separa un archivo que el
|
|
81
81
|
// borrado no tocó de uno que alguien reescribió mientras borrábamos.
|
package/engine/cli/catalog.js
CHANGED
|
@@ -13,13 +13,13 @@ 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 } = require('./io')
|
|
16
|
+
const { fail, opsRoot, TODAY } = require('./io')
|
|
17
17
|
|
|
18
18
|
function agentsFork(slug, dir) {
|
|
19
19
|
const root = opsRoot(dir)
|
|
20
20
|
if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', 2)
|
|
21
21
|
let result
|
|
22
|
-
const date =
|
|
22
|
+
const date = TODAY()
|
|
23
23
|
try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message, 2) }
|
|
24
24
|
console.log(`+ ${path.relative(root, result.dir)} (${result.files.length} archivo(s))`)
|
|
25
25
|
if (result.skipped.length) {
|
|
@@ -179,7 +179,7 @@ function evaluate(agent, caso, cli) {
|
|
|
179
179
|
// Dónde escribir el registro de esta corrida. Lo pregunta el recorrido en vez de componer el
|
|
180
180
|
// nombre, que es lo que hacía que la segunda corrida de un día borrara a la primera.
|
|
181
181
|
if (cli.has('--record')) {
|
|
182
|
-
const day = cli.value('--record') ||
|
|
182
|
+
const day = cli.value('--record') || TODAY()
|
|
183
183
|
return console.log(path.relative(root,
|
|
184
184
|
path.join(EV.resultsDir(root, agent, kind), EV.nextResult(root, agent, day, kind))))
|
|
185
185
|
}
|
package/engine/cli/claims.js
CHANGED
|
@@ -9,9 +9,7 @@ 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 } = require('./io')
|
|
13
|
-
|
|
14
|
-
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
12
|
+
const { fail, planningRoot, TODAY } = require('./io')
|
|
15
13
|
|
|
16
14
|
function claim(dir, slug, cli) {
|
|
17
15
|
const root = planningRoot(dir)
|
|
@@ -22,8 +20,8 @@ function claim(dir, slug, cli) {
|
|
|
22
20
|
|
|
23
21
|
// No se reserva lo que todavía no se puede empezar: una tarea tomada con su dependencia en vuelo
|
|
24
22
|
// bloquea la cola sin que nadie pueda avanzarla, y el runner que la tomó se queda sin poder tomar otra.
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
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.`)
|
|
27
25
|
|
|
28
26
|
const me = CL.owner(root)
|
|
29
27
|
const from = CL.runner()
|
|
@@ -45,7 +43,7 @@ function claim(dir, slug, cli) {
|
|
|
45
43
|
|
|
46
44
|
const target = CL.file(root, slug)
|
|
47
45
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
48
|
-
const
|
|
46
|
+
const body = CL.content({ task: slug, owner: me, runner: from, started: TODAY(), service: task.service })
|
|
49
47
|
try {
|
|
50
48
|
// Reservar **es** crear el archivo, así que el único juez de quién la tiene es el archivo. `wx` falla
|
|
51
49
|
// si ya está, y de ahí sale la respuesta entera: propia, ajena o perdida en la carrera.
|
|
@@ -54,25 +52,25 @@ function claim(dir, slug, cli) {
|
|
|
54
52
|
// de diferencia ganarían los dos sin que ninguno se entere. Y una comprobación previa tampoco
|
|
55
53
|
// alcanzaría —entre mirar y escribir queda la misma ventana—, así que sería un segundo juez que
|
|
56
54
|
// adelanta un veredicto que este bloque tiene que volver a dar igual.
|
|
57
|
-
fs.writeFileSync(target,
|
|
55
|
+
fs.writeFileSync(target, body, { flag: 'wx' })
|
|
58
56
|
} catch (error) {
|
|
59
57
|
if (error.code !== 'EEXIST') throw error
|
|
60
|
-
const
|
|
58
|
+
const holder = CL.read(root).find((one) => one.slug === slug)
|
|
61
59
|
// Existía al crear y ya no está: alguien la soltó entre las dos operaciones. Es una ventana de
|
|
62
60
|
// microsegundos y aun así tiene respuesta, porque la alternativa es reventar con un TypeError.
|
|
63
|
-
if (!
|
|
64
|
-
if (
|
|
61
|
+
if (!holder) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`)
|
|
62
|
+
if (holder.runner === from) return console.log(`= ${slug} ya era tuya desde ${holder.started}`)
|
|
65
63
|
// Mismo dueño y otro runner son dos situaciones que se ven idénticas desde acá —vos retomando la
|
|
66
64
|
// sesión de ayer, o un segundo agente tuyo— y ninguna se puede distinguir mirando el archivo.
|
|
67
65
|
// Retomarla sola le sacaría la tarea al otro agente; crear un runner nuevo dejaría dos trabajando lo
|
|
68
66
|
// mismo. Las dos rompen trabajo, así que decide una persona y acá sólo se dice cuál es cuál.
|
|
69
|
-
if (
|
|
70
|
-
return fail(`${slug} la tenés vos, tomada el ${
|
|
67
|
+
if (holder.owner === me) {
|
|
68
|
+
return fail(`${slug} la tenés vos, tomada el ${holder.started} desde el runner ${holder.runner}. `
|
|
71
69
|
+ 'Preguntá si se retoma esa sesión —y entonces corré con ese id— o si es otro agente en '
|
|
72
70
|
+ `paralelo, que toma otra tarea. \`ops runners ${path.relative(process.cwd(), root) || '.'}\` `
|
|
73
71
|
+ 'lista lo que hay abierto.')
|
|
74
72
|
}
|
|
75
|
-
return fail(`${slug} la tomó ${
|
|
73
|
+
return fail(`${slug} la tomó ${holder.owner} el ${holder.started}. Si se abandonó, borrá `
|
|
76
74
|
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`)
|
|
77
75
|
}
|
|
78
76
|
console.log(`✓ ${slug} tomada por ${me}`)
|
|
@@ -111,20 +109,20 @@ function release(dir, slug) {
|
|
|
111
109
|
function runners(dir, cli) {
|
|
112
110
|
const root = planningRoot(dir)
|
|
113
111
|
const done = ST.snapshot(root).done
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
112
|
+
const open = CL.read(root).filter((one) => !done.set.has(one.slug))
|
|
113
|
+
const today = TODAY()
|
|
114
|
+
const rows = open.map((one) => {
|
|
117
115
|
const commit = R.lastCommit(R.repoOf(path.join(root, '..'), one.service), CL.branchOf(one.slug))
|
|
118
116
|
return { runner: one.runner, task: one.slug, owner: one.owner, started: one.started, lastCommit: commit }
|
|
119
117
|
})
|
|
120
|
-
if (cli.has('--json')) return console.log(JSON.stringify(
|
|
121
|
-
if (!
|
|
122
|
-
const
|
|
123
|
-
for (const
|
|
124
|
-
const
|
|
125
|
-
console.log(`${
|
|
118
|
+
if (cli.has('--json')) return console.log(JSON.stringify(rows))
|
|
119
|
+
if (!rows.length) return console.log('= ningún runner tiene trabajo abierto: arrancá con un id propio')
|
|
120
|
+
const width = Math.max(...rows.map((one) => one.runner.length))
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
const progress = row.lastCommit ? `último commit ${row.lastCommit}` : 'sin commits en su rama'
|
|
123
|
+
console.log(`${row.runner.padEnd(width)} ${row.task} (${row.owner}, desde ${row.started}; ${progress})`)
|
|
126
124
|
}
|
|
127
|
-
console.log(`\n${
|
|
125
|
+
console.log(`\n${rows.length} runner(s) con trabajo abierto al ${today}. `
|
|
128
126
|
+ 'Retomá uno usando su id, o arrancá con uno propio.')
|
|
129
127
|
}
|
|
130
128
|
|
package/engine/cli/contract.js
CHANGED
|
@@ -83,9 +83,18 @@ const MARKED = /^###\s+Límites\s*$/m
|
|
|
83
83
|
// llevaba adentro los cuatro párrafos que la persona había agregado al final de la sección. Descontar el
|
|
84
84
|
// bloque entero —que es lo que parecía el arreglo— apagaba justo lo que el aviso existe para encontrar.
|
|
85
85
|
const BULLET = /^\s*[-*]\s+/
|
|
86
|
+
// Una viñeta es la viñeta entera, no su primera línea. Un límite de verdad no entra en el ancho del
|
|
87
|
+
// archivo, así que se escribe en dos, y recorriendo línea por línea lo que viajaba al preámbulo de cada
|
|
88
|
+
// subagente era la mitad — cortada justo donde suele estar lo que el límite decide (caso 168).
|
|
89
|
+
//
|
|
90
|
+
// `own` son las líneas **sin plegar**, y existe para `warnings`: descuenta por línea contra lo que el
|
|
91
|
+
// archivo tiene escrito (caso 159), y una viñeta ya plegada no coincide con ninguna de esas líneas, así
|
|
92
|
+
// que su continuación volvía a contarse como un párrafo perdido. Son dos preguntas distintas sobre el
|
|
93
|
+
// mismo bloque: qué dice cada límite, y qué líneas ya están cubiertas.
|
|
86
94
|
function marked(raw) {
|
|
87
95
|
const bullets = []
|
|
88
96
|
const intros = []
|
|
97
|
+
const own = []
|
|
89
98
|
let rest = raw
|
|
90
99
|
for (let start = rest.search(MARKED); start >= 0; start = rest.search(MARKED)) {
|
|
91
100
|
const after = rest.slice(start).split('\n').slice(1)
|
|
@@ -93,13 +102,21 @@ function marked(raw) {
|
|
|
93
102
|
const block = end < 0 ? after : after.slice(0, end)
|
|
94
103
|
const first = block.findIndex((line) => BULLET.test(line))
|
|
95
104
|
if (first > 0) intros.push(...block.slice(0, first).map((line) => line.trim()).filter(Boolean))
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
// Una línea en blanco cierra la viñeta abierta: lo que venga después es otra cosa y pegarlo ahí
|
|
106
|
+
// uniría dos límites en uno.
|
|
107
|
+
let open = -1
|
|
108
|
+
for (const line of first < 0 ? [] : block.slice(first)) {
|
|
109
|
+
const text = line.replace(BULLET, '').trim()
|
|
110
|
+
if (!text) { open = -1; continue }
|
|
111
|
+
// A `own` va lo que pertenece a una viñeta y nada más. Empujar toda línea no vacía haría que la
|
|
112
|
+
// prosa suelta que quedó bajo el encabezado contara como declarada y `warnings` dejara de nombrarla:
|
|
113
|
+
// el plegado apagaría el aviso en vez de arreglar el límite.
|
|
114
|
+
if (BULLET.test(line)) { bullets.push(text); open = bullets.length - 1; own.push(text) }
|
|
115
|
+
else if (open >= 0) { bullets[open] += ` ${text}`; own.push(text) }
|
|
116
|
+
}
|
|
100
117
|
rest = (end < 0 ? '' : after.slice(end).join('\n'))
|
|
101
118
|
}
|
|
102
|
-
return { bullets, intros }
|
|
119
|
+
return { bullets, intros, own }
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
const declared = (raw) => marked(raw).bullets
|
|
@@ -147,8 +164,8 @@ function warnings(root) {
|
|
|
147
164
|
// Se descuenta por línea y no por párrafo, que es donde estaba el defecto: `paragraphs` saca el `- ` y
|
|
148
165
|
// une las viñetas seguidas en un párrafo solo, así que una lista declarada no era igual a ninguna
|
|
149
166
|
// entrada de `declared()` y se contaba entera como un límite perdido (caso 159).
|
|
150
|
-
const {
|
|
151
|
-
const suyo = new Set([...
|
|
167
|
+
const { own, intros } = marked(mine)
|
|
168
|
+
const suyo = new Set([...own, ...intros])
|
|
152
169
|
const outside = P.withoutComments(mine).split('\n')
|
|
153
170
|
.filter((line) => !suyo.has(line.replace(BULLET, '').trim()))
|
|
154
171
|
.join('\n')
|
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 } = require('./io')
|
|
19
19
|
const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
|
|
20
20
|
const { adviceFor, previewUpgrade, reportUpgrade } = require('./upgrade-report')
|
|
21
21
|
|
|
@@ -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
|
|
@@ -309,17 +309,17 @@ function upgrade(dir, cli) {
|
|
|
309
309
|
//
|
|
310
310
|
// El aviso sale en **cada** corrida y no sólo la primera: una instancia con medio molde congelado y
|
|
311
311
|
// 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([...
|
|
312
|
+
const keptFiles = force ? new Set() : new Set(changed)
|
|
313
|
+
if (keptFiles.size) {
|
|
314
|
+
for (const file of keptFiles) console.log(`= conservado ${file} (editado localmente)`)
|
|
315
|
+
console.log(`\n${keptFiles.size} archivo(s) del molde quedan congelados por tu edición.`)
|
|
316
|
+
console.log(`${adviceFor([...keptFiles])}\n`)
|
|
317
317
|
console.log('Para tomar la versión nueva y descartar la tuya, repetí con --force.\n')
|
|
318
318
|
}
|
|
319
319
|
// Un archivo propio que se llama como uno que el paquete empieza a traer (caso 110): se conserva y se
|
|
320
320
|
// dice, igual que una edición local, y `--force` lo reemplaza diciéndolo.
|
|
321
|
-
const
|
|
322
|
-
for (const file of
|
|
321
|
+
const collisions = new Set(force ? [] : colliding)
|
|
322
|
+
for (const file of collisions) {
|
|
323
323
|
console.log(`= conservado ${file}: ya existía y Cauce no lo entregó, así que el del paquete no se `
|
|
324
324
|
+ 'instaló. Renombrá el tuyo y repetí, o repetí con --force para reemplazarlo.')
|
|
325
325
|
}
|
|
@@ -345,7 +345,7 @@ function upgrade(dir, cli) {
|
|
|
345
345
|
'{{PROJECT_NAME}}': config.project || path.basename(root),
|
|
346
346
|
'{{MODE}}': O.mode(root),
|
|
347
347
|
'{{WORKSPACE_PATH}}': O.mode(root) === 'embedded' ? '.' : '..',
|
|
348
|
-
...RC.sinceValues(
|
|
348
|
+
...RC.sinceValues(TODAY()),
|
|
349
349
|
})) content = content.replaceAll(key, value)
|
|
350
350
|
F.atomicWrite(target, content)
|
|
351
351
|
added.push(relative)
|
|
@@ -368,13 +368,13 @@ function upgrade(dir, cli) {
|
|
|
368
368
|
|
|
369
369
|
const conservar = (file) => {
|
|
370
370
|
const relative = path.relative(root, file).replace(/\\/g, '/')
|
|
371
|
-
return
|
|
371
|
+
return keptFiles.has(relative) || collisions.has(relative)
|
|
372
372
|
}
|
|
373
373
|
for (const relative of [...system, ...O.RUNTIME_PATHS]) {
|
|
374
374
|
const origin = path.join(PROJECT_ROOT, O.sourceOf(relative))
|
|
375
375
|
if (!fs.existsSync(origin)) continue
|
|
376
376
|
const target = path.join(root, relative)
|
|
377
|
-
if (
|
|
377
|
+
if (keptFiles.has(relative)) continue
|
|
378
378
|
// Sobrescribe lo que trae el paquete y deja intacto lo demás: un guard propio de la empresa,
|
|
379
379
|
// o un adaptador de runner que el toolkit no conoce, sobreviven a la actualización.
|
|
380
380
|
if (fs.statSync(origin).isDirectory()) copyRuntime(origin, target, false, root, [], conservar)
|
|
@@ -389,7 +389,7 @@ function upgrade(dir, cli) {
|
|
|
389
389
|
|
|
390
390
|
// Retirar lo que el toolkit ya no distribuye, después de haber actualizado lo que sí.
|
|
391
391
|
const retired = []
|
|
392
|
-
const
|
|
392
|
+
const pending = []
|
|
393
393
|
for (const relative of O.RETIRED) {
|
|
394
394
|
const target = path.join(root, relative)
|
|
395
395
|
if (!fs.existsSync(target)) continue
|
|
@@ -398,10 +398,10 @@ function upgrade(dir, cli) {
|
|
|
398
398
|
// también usa para lo suyo, borrar el directorio entero se llevaba puesto contenido que nadie
|
|
399
399
|
// había entregado —un `autobuild.js` propio, los workflows de una empresa— sin confirmación y sin
|
|
400
400
|
// vuelta atrás. Se conserva y se nombra; `--force` es la salida, igual que para una edición local.
|
|
401
|
-
const
|
|
401
|
+
const leftover = O.RETIRED_SHARED.includes(relative) && fs.statSync(target).isDirectory()
|
|
402
402
|
? O.treeFiles(target)
|
|
403
403
|
: []
|
|
404
|
-
if (
|
|
404
|
+
if (leftover.length && !force) { pending.push({ relative, files: leftover }); continue }
|
|
405
405
|
fs.rmSync(target, { recursive: true, force: true })
|
|
406
406
|
retired.push(relative)
|
|
407
407
|
}
|
|
@@ -413,7 +413,7 @@ function upgrade(dir, cli) {
|
|
|
413
413
|
// conservar **ese** digest: registrar el de disco lo volvería idéntico a lo entregado, dejaría de
|
|
414
414
|
// detectarse como editado y la corrida siguiente lo pisaría sin decir nada. Es el 001 de vuelta por
|
|
415
415
|
// la puerta de atrás, y no se ve mirando el archivo — se ve dos upgrades después.
|
|
416
|
-
const
|
|
416
|
+
const delivered = { ...record }
|
|
417
417
|
for (const relative of O.trackedPaths()) {
|
|
418
418
|
if (fs.existsSync(path.join(root, relative))) {
|
|
419
419
|
record = M.record(root, relative, O.deliveredFiles(root, relative), record)
|
|
@@ -428,8 +428,8 @@ function upgrade(dir, cli) {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
record = M.recordPaths(root, O.SYSTEM_FILES, record)
|
|
431
|
-
for (const file of
|
|
432
|
-
for (const file of
|
|
431
|
+
for (const file of keptFiles) if (delivered[file]) record[file] = delivered[file]
|
|
432
|
+
for (const file of collisions) delete record[file]
|
|
433
433
|
// El registro de forks se poda igual que el de archivos: un cargo devuelto al catálogo deja su
|
|
434
434
|
// entrada, y una entrada sin copia sólo puede producir avisos sobre algo que no está.
|
|
435
435
|
const kept = Object.fromEntries(Object.entries(M.readForks(root)).filter(
|
|
@@ -447,7 +447,7 @@ function upgrade(dir, cli) {
|
|
|
447
447
|
// que no ocurrieron (caso 048).
|
|
448
448
|
reportUpgrade({
|
|
449
449
|
root, from, to, system, retired, added, overrides, pinned, droppedBlocks,
|
|
450
|
-
|
|
450
|
+
discarded: force ? changed : [], keptFiles: [...keptFiles], pending,
|
|
451
451
|
})
|
|
452
452
|
}
|
|
453
453
|
|
package/engine/cli/ops.js
CHANGED
|
@@ -195,11 +195,15 @@ const NO_FLAGS = { has: () => false, value: (_flag, fallback = '') => fallback }
|
|
|
195
195
|
|
|
196
196
|
async function run(cli) {
|
|
197
197
|
const [command] = cli.positional
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// `--help` y `-h` nunca llegan acá: `parse` los reconoce como banderas, así que el posicional queda
|
|
199
|
+
// vacío y lo atiende el `!command` de al lado. Lo que sí es un posicional es `help` a secas.
|
|
200
|
+
if (!command || command === 'help') return usage()
|
|
201
|
+
// `Object.hasOwn` y no `FLAGS[command]`: `constructor` heredado de `Object.prototype` pasaba por
|
|
202
|
+
// 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) }
|
|
200
204
|
// `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
|
|
201
205
|
// actual en vez de explicarse.
|
|
202
|
-
if (cli.has('--help')
|
|
206
|
+
if (cli.has('--help')) return usage()
|
|
203
207
|
const unknown = cli.unknown(command)
|
|
204
208
|
if (unknown.length) {
|
|
205
209
|
const accepts = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
|
package/engine/cli/planning.js
CHANGED
|
@@ -141,7 +141,7 @@ 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)
|
|
@@ -150,7 +150,7 @@ function context(dir, cli) {
|
|
|
150
150
|
const hay = state.milestones.map((one) => one.slug).join(', ') || '(ninguno)'
|
|
151
151
|
return fail(`el hito ${hito} no existe. Hay: ${hay}`, 2)
|
|
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}\``)
|
|
@@ -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
|
@@ -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, '..'))
|