@ingeniomaps/cauce 0.95.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/engine/automation/check.js +6 -0
- package/engine/cli/archive.js +3 -3
- package/engine/cli/bench.js +6 -6
- package/engine/cli/catalog.js +34 -15
- package/engine/cli/claims.js +12 -12
- package/engine/cli/contract.js +5 -4
- package/engine/cli/dependency.js +2 -2
- package/engine/cli/instance.js +7 -6
- package/engine/cli/io.js +26 -4
- package/engine/cli/ops.js +18 -13
- package/engine/cli/planning.js +4 -4
- package/engine/cli/validate.js +2 -2
- package/engine/cli/wiring.js +16 -16
- package/engine/cli/worktree.js +8 -8
- package/engine/core/ownership.js +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,48 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.96.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Tres errores que el CLI contestaba con exit 1 ahora contestan 2, que es lo que dice la convención.**
|
|
22
|
+
Son «no se llegó a la pregunta»: `integrations/config.json` ilegible, un proveedor que no está en ese
|
|
23
|
+
registro, y un `package.json` inválido al declarar el motor. Sus hermanos —un `ops.config.json`
|
|
24
|
+
ilegible, un proveedor que Cauce no trae— ya contestaban 2.
|
|
25
|
+
|
|
26
|
+
El corte, que hasta ahora estaba en la cabeza de quien escribía cada salida, está escrito y sale en
|
|
27
|
+
`ops --help`: **2** es que el comando no existe, falta un argumento, o la raíz que nombrás no es lo que
|
|
28
|
+
dice ser —lo arreglás cambiando la invocación—; **1** es que se llegó y la respuesta es que no —una
|
|
29
|
+
validación encontró problemas, el estado se niega, o una operación falló a mitad de camino—.
|
|
30
|
+
|
|
31
|
+
Si tenés un script que distingue los dos, revisá esos tres casos. Si sólo mira «distinto de cero», no
|
|
32
|
+
cambia nada.
|
|
33
|
+
|
|
34
|
+
- **`ops flow list|check|show` acepta la raíz de la instancia, como el resto.** Todos los comandos que leen
|
|
35
|
+
una instancia la toman como posicional; `flow` la descartaba y resolvía por el directorio actual, así que
|
|
36
|
+
`ops flow list <raíz>` contestaba sobre otra cosa — y una lista de recorridos se lee igual de bien venga
|
|
37
|
+
de donde venga. Ahora es `ops flow list [ops-root]`, y con recorrido va después de él.
|
|
38
|
+
|
|
39
|
+
- **Una lista de cargos o recorridos vacía dice si es porque no hay o porque no se pudo resolver el
|
|
40
|
+
paquete.** Eran dos hechos distintos con la misma respuesta, y las acciones son opuestas. El aviso sale
|
|
41
|
+
por `stderr` y sólo cuando la lista viene vacía; el código de salida y el `--json` no cambian, porque
|
|
42
|
+
ese JSON lo consume el cron del ciclo de aprendizaje.
|
|
43
|
+
|
|
44
|
+
- **`automation check` nombra un `ops.config.json` ilegible en vez de culpar al motor.** La resolución del
|
|
45
|
+
paquete cuelga de esa configuración, así que un JSON roto se propagaba como archivos del motor que faltan
|
|
46
|
+
y el aviso mandaba a correr `npm install` sobre un motor instalado: la acción sugerida no arreglaba nada.
|
|
47
|
+
|
|
48
|
+
- **El catálogo de cargos y recorridos se encuentra también cuando el paquete vive un nivel arriba.** Es el
|
|
49
|
+
layout que documenta el arranque —`npm install @ingeniomaps/cauce` en la carpeta de la empresa y la
|
|
50
|
+
instancia adentro—, y 0.92.0 lo arregló para el motor y dejó afuera el catálogo. En ese estado `check`
|
|
51
|
+
pasaba en verde y `agents list` contestaba **una lista vacía con exit 0**, indistinguible de «esta
|
|
52
|
+
instancia no tiene cargos»; `flow list` igual, y `evaluate` culpaba a un `SKILL.md` que falta, mandando
|
|
53
|
+
a mirar el cargo en vez del paquete.
|
|
54
|
+
|
|
55
|
+
Si te pasó, no hacía falta bajar una segunda copia: con esta versión los cargos aparecen donde ya estaban.
|
|
56
|
+
Verificado instalando el paquete publicado desde el registro y recorriendo la superficie del CLI: de 0
|
|
57
|
+
cargos y 0 recorridos a **53 y 7**, sobre la misma instancia.
|
|
58
|
+
|
|
17
59
|
## [0.95.0] - 2026-09-16
|
|
18
60
|
|
|
19
61
|
### Corregido
|
|
@@ -22,6 +22,12 @@ const { hasHooks } = require('./config')
|
|
|
22
22
|
|
|
23
23
|
function check(root) {
|
|
24
24
|
const errors = []
|
|
25
|
+
// La configuración se lee primero y se corta ahí. La resolución del paquete cuelga de ella —`declaredRoot`
|
|
26
|
+
// la parsea— así que un JSON roto se propagaba como archivos del motor que faltan, y el aviso mandaba a
|
|
27
|
+
// correr `npm install` sobre un motor que está instalado: la acción sugerida no arreglaba nada. Nombrar
|
|
28
|
+
// la causa y no seguir es lo que separa este aviso del ruido — enumerar ausencias que salen de una causa
|
|
29
|
+
// ya nombrada manda a arreglar lo que no está roto (caso 174).
|
|
30
|
+
try { O.mode(root) } catch (error) { return [error.message] }
|
|
25
31
|
const hookDir = path.join(root, 'automatization', 'hooks')
|
|
26
32
|
if (!fs.existsSync(path.join(root, 'automatization', 'AGENTS.md'))) {
|
|
27
33
|
errors.push('falta automatization/AGENTS.md')
|
package/engine/cli/archive.js
CHANGED
|
@@ -11,7 +11,7 @@ const P = require('../planning/parser')
|
|
|
11
11
|
const PC = require('../planning/contracts')
|
|
12
12
|
const AD = require('../planning/adoption')
|
|
13
13
|
const F = require('../core/files')
|
|
14
|
-
const { fail, planningRoot, TODAY } = require('./io')
|
|
14
|
+
const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
|
|
15
15
|
|
|
16
16
|
// El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
|
|
17
17
|
// a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
|
|
@@ -30,7 +30,7 @@ function adopt(dir) {
|
|
|
30
30
|
const existing = fs.readFileSync(target, 'utf8')
|
|
31
31
|
if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
|
|
32
32
|
fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
|
|
33
|
-
+ 'delante; `check` marca los que ya cumplen.')
|
|
33
|
+
+ 'delante; `check` marca los que ya cumplen.', REFUSED)
|
|
34
34
|
}
|
|
35
35
|
const slugs = AD.declared(existing)
|
|
36
36
|
F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
|
|
@@ -78,7 +78,7 @@ function archiveHumanActions(root) {
|
|
|
78
78
|
function archive(dir, rawNum) {
|
|
79
79
|
if (String(rawNum || '') === 'human-actions') return archiveHumanActions(planningRoot(dir))
|
|
80
80
|
return fail('Sólo se archiva `human-actions`. La evidencia de una tarea ya vive en su propio archivo '
|
|
81
|
-
+ 'de `done/`, así que archivar una épica dejó de tener sentido.',
|
|
81
|
+
+ 'de `done/`, así que archivar una épica dejó de tener sentido.', USAGE)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
module.exports = { archive, adopt }
|
package/engine/cli/bench.js
CHANGED
|
@@ -18,7 +18,7 @@ const CL = require('../planning/claims')
|
|
|
18
18
|
const P = require('../planning/parser')
|
|
19
19
|
const IN = require('./instance')
|
|
20
20
|
const O = require('../core/ownership')
|
|
21
|
-
const { fail, opsRoot, TODAY } = require('./io')
|
|
21
|
+
const { fail, opsRoot, TODAY, USAGE } = require('./io')
|
|
22
22
|
|
|
23
23
|
// Qué decir cuando el banco sobrevivió a su propio borrado, que es lo único que va a permitir
|
|
24
24
|
// establecer la causa. Devuelve el mensaje en vez de escribirlo donde ocurre, y eso es lo que lo hace
|
|
@@ -106,7 +106,7 @@ function makeBench(root, dir, force, name) {
|
|
|
106
106
|
const dirty = spawnSync('git', ['-C', dir, 'status', '--porcelain'], { encoding: 'utf8' })
|
|
107
107
|
if ((dirty.stdout || '').trim() && !force) {
|
|
108
108
|
fail(`${dir} tiene trabajo sin recoger. Guardá lo que esa corrida dejó antes de rehacerlo, `
|
|
109
|
-
+ 'o usá --force si ya lo tenés.',
|
|
109
|
+
+ 'o usá --force si ya lo tenés.', USAGE)
|
|
110
110
|
}
|
|
111
111
|
// Rodear un borrado a medias deja la corrida siguiendo sobre un banco que no es nuevo, y lo que falla
|
|
112
112
|
// después no dice nada del borrado: el test que lo destapó reportaba `true !== false` sobre un archivo
|
|
@@ -116,7 +116,7 @@ function makeBench(root, dir, force, name) {
|
|
|
116
116
|
// **Y de acá para abajo el directorio no existe.** Eso es lo que sostiene que el andamiaje y el enlace
|
|
117
117
|
// se escriban sin defensas: hasta el 073, los dos llevaban una por si algo sobrevivía al borrado.
|
|
118
118
|
const problema = clearBench(dir, path.join(root, '.cauce-eval'))
|
|
119
|
-
if (problema) fail(problema,
|
|
119
|
+
if (problema) fail(problema, USAGE)
|
|
120
120
|
// Sin `force`, y eso es lo que hay que poder decir: sólo servía si algún archivo sobrevivía al borrado,
|
|
121
121
|
// y la comprobación de arriba garantiza que no queda ninguno. Lo llevaba porque el mismo test falló tres
|
|
122
122
|
// veces en un día con «El destino contiene …/AGENTS.md», y eso era el escritor de fondo que apagó el 073.
|
|
@@ -183,7 +183,7 @@ function seal(dir, git, mensaje) {
|
|
|
183
183
|
function evaluationBench(root, agent, caso, force, kind) {
|
|
184
184
|
const safe = (value) => {
|
|
185
185
|
if (!/^[a-z0-9_][a-z0-9._-]*$/i.test(value) || value.includes('..')) {
|
|
186
|
-
fail(`nombre inválido para el banco: ${value}`,
|
|
186
|
+
fail(`nombre inválido para el banco: ${value}`, USAGE)
|
|
187
187
|
}
|
|
188
188
|
return value
|
|
189
189
|
}
|
|
@@ -273,7 +273,7 @@ function populate(dir, scenario, git) {
|
|
|
273
273
|
// `clearBench` ya se niega a borrar fuera de ahí— y se distingue por el escenario, que es lo que lo puebla.
|
|
274
274
|
function measurementBench(root, scenario, force) {
|
|
275
275
|
if (!SCENARIOS.includes(scenario)) {
|
|
276
|
-
fail(`escenario desconocido: ${scenario || '(ninguno)'}. Hay ${SCENARIOS.join(', ')}.`,
|
|
276
|
+
fail(`escenario desconocido: ${scenario || '(ninguno)'}. Hay ${SCENARIOS.join(', ')}.`, USAGE)
|
|
277
277
|
}
|
|
278
278
|
const dir = path.join(root, '.cauce-eval', '_medicion', scenario)
|
|
279
279
|
const { env, git } = makeBench(root, dir, force, `Banco de medición (${scenario})`)
|
|
@@ -293,7 +293,7 @@ function bench(scenario, cli) {
|
|
|
293
293
|
const root = opsRoot()
|
|
294
294
|
if (O.mode(root) !== 'toolkit') {
|
|
295
295
|
fail('ops bench es del toolkit: arma un banco desechable para medir a Cauce. En una instancia, lo '
|
|
296
|
-
+ 'que se mide es tu propio proyecto — corré el comando que quieras medir sobre tu planning/.',
|
|
296
|
+
+ 'que se mide es tu propio proyecto — corré el comando que quieras medir sobre tu planning/.', USAGE)
|
|
297
297
|
}
|
|
298
298
|
const dir = measurementBench(root, scenario, cli.has('--force'))
|
|
299
299
|
console.log(path.relative(root, dir))
|
package/engine/cli/catalog.js
CHANGED
|
@@ -13,14 +13,14 @@ const O = require('../core/ownership')
|
|
|
13
13
|
// El banco desechable y su borrado comprobado. Se reexportan abajo porque su contrato lo fija la suite
|
|
14
14
|
// del banco, que llega por acá desde antes de que el módulo existiera.
|
|
15
15
|
const B = require('./bench')
|
|
16
|
-
const { fail, opsRoot, TODAY } = require('./io')
|
|
16
|
+
const { fail, opsRoot, TODAY, USAGE, REFUSED } = require('./io')
|
|
17
17
|
|
|
18
18
|
function agentsFork(slug, dir) {
|
|
19
19
|
const root = opsRoot(dir)
|
|
20
|
-
if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]',
|
|
20
|
+
if (!slug) fail('Falta el cargo: ops agents fork <cargo> [ops-root]', USAGE)
|
|
21
21
|
let result
|
|
22
22
|
const date = TODAY()
|
|
23
|
-
try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message,
|
|
23
|
+
try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message, USAGE) }
|
|
24
24
|
console.log(`+ ${path.relative(root, result.dir)} (${result.files.length} archivo(s))`)
|
|
25
25
|
if (result.skipped.length) {
|
|
26
26
|
console.log(` quedan en el catálogo: ${result.skipped.length} artefacto(s) que ganó su versión`)
|
|
@@ -31,15 +31,28 @@ function agentsFork(slug, dir) {
|
|
|
31
31
|
|
|
32
32
|
// Lista los cargos visibles resolviendo la precedencia; evita que cada consumidor —CI incluido—
|
|
33
33
|
// reimplemente el recorrido del catálogo.
|
|
34
|
+
// Una lista vacía y un paquete que no se resolvió se ven igual, y las acciones son opuestas: en uno no hay
|
|
35
|
+
// nada que hacer, en el otro falta una instalación. Se avisa sólo cuando la lista sale vacía, que es cuando
|
|
36
|
+
// la ambigüedad existe — con resultados a la vista no hay nada que aclarar.
|
|
37
|
+
//
|
|
38
|
+
// Va por `stderr` y no por el código de salida: el `--json` lo consume el cron del ciclo de aprendizaje, y
|
|
39
|
+
// romperle el contrato para arreglar un mensaje sería cambiar lo que no está mal (caso 173).
|
|
40
|
+
function warnUnresolved(root, name, found) {
|
|
41
|
+
if (found || O.packageDir(root, name)) return
|
|
42
|
+
console.error(`⚠ ${name}/ no se pudo resolver desde ${root}: el paquete @ingeniomaps/cauce no está ahí `
|
|
43
|
+
+ `ni en la raíz que declara ops.config.json, así que una lista vacía acá no dice que no haya ${name}.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
function agents(action, dir, extra, cli) {
|
|
35
47
|
if (action === 'fork') return agentsFork(dir, extra)
|
|
36
|
-
if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`,
|
|
48
|
+
if (action !== 'list') fail(`Acción de agents desconocida: ${action || '(vacía)'}`, USAGE)
|
|
37
49
|
const root = opsRoot(dir)
|
|
38
50
|
// Una empresa mantiene sus cargos, no los nuestros: `learn` sobre uno del catálogo se niega, así que
|
|
39
51
|
// recorrer el catálogo entero para encontrar el suyo es ruido. `--own` hace ejecutable ese recorrido.
|
|
40
52
|
const own = cli.has('--own')
|
|
41
53
|
const system = cli.has('--system')
|
|
42
54
|
const roles = AG.list(root).filter((role) => (own ? !role.system : true) && (system ? role.system : true))
|
|
55
|
+
warnUnresolved(root, 'agents', roles.length)
|
|
43
56
|
if (cli.has('--json')) {
|
|
44
57
|
// `path` viene resuelto: quien consuma esto no debería reconstruir dónde ganó la precedencia.
|
|
45
58
|
return console.log(JSON.stringify(roles.map((role) => ({
|
|
@@ -104,7 +117,7 @@ function learn(agent, cli) {
|
|
|
104
117
|
// falta. Negarse nombrando el comando que sí corresponde es lo que cierra R13.
|
|
105
118
|
if (kind === 'flow' && !cli.has('--proposal')) {
|
|
106
119
|
fail(`${agent} es un recorrido: aprende de sus corridas, no de informes semanales.\n`
|
|
107
|
-
+ ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`,
|
|
120
|
+
+ ` Abrí la propuesta con "ops learn ${agent} --flow --proposal".`, USAGE)
|
|
108
121
|
}
|
|
109
122
|
// `--period` es para consolidar a mano un mes que no es el de hoy. El ciclo automático no lo
|
|
110
123
|
// pasa: la propuesta se llama por el mes en que se abre y arrastra lo que todavía no entró.
|
|
@@ -133,7 +146,7 @@ function learn(agent, cli) {
|
|
|
133
146
|
if (LF.blankProposal(result.file)) {
|
|
134
147
|
console.log(' sin cambio decidido: falta correr agent-propose antes de que esto se pueda firmar')
|
|
135
148
|
}
|
|
136
|
-
} catch (error) { fail(error.message,
|
|
149
|
+
} catch (error) { fail(error.message, USAGE) }
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
function evaluate(agent, caso, cli) {
|
|
@@ -149,7 +162,7 @@ function evaluate(agent, caso, cli) {
|
|
|
149
162
|
// forkeaba, repetía el comando y recibía el mismo mensaje diciéndole que forkeara—.
|
|
150
163
|
fail('--bench es del toolkit. En una instancia, el cargo trabaja sobre tu planning/: corré '
|
|
151
164
|
+ `"ops evaluate ${agent}" sin la bandera, que valida sus controles, casos y propuestas `
|
|
152
|
-
+ 'contra este proyecto.',
|
|
165
|
+
+ 'contra este proyecto.', USAGE)
|
|
153
166
|
}
|
|
154
167
|
// El caso es el posicional que sigue al cargo: `evaluate <cargo> --bench <caso>`. Sin él se arma
|
|
155
168
|
// un banco suelto, para mirarlo a mano; una corrida real pide uno por caso.
|
|
@@ -190,7 +203,7 @@ function evaluate(agent, caso, cli) {
|
|
|
190
203
|
const errors = [...result.errors, ...runs.errors]
|
|
191
204
|
for (const warning of [...result.warnings, ...runs.warnings]) console.warn(`⚠ ${warning}`)
|
|
192
205
|
for (const error of errors) console.error(`✗ ${error}`)
|
|
193
|
-
if (errors.length) fail(`\n${errors.length} error(es)`, 1)
|
|
206
|
+
if (errors.length) fail(`\n${errors.length} error(es)`, 1, REFUSED)
|
|
194
207
|
// Cuándo se midió es un rango cuando el veredicto vigente lo aportó más de una corrida. Por qué se
|
|
195
208
|
// compone en vez de leerse la última, en `composed`.
|
|
196
209
|
const measuredAt = runs.state && runs.state.oldest !== runs.state.newest
|
|
@@ -208,13 +221,19 @@ function evaluate(agent, caso, cli) {
|
|
|
208
221
|
`${result.pending ? ` (${result.pending} sin aplicar)` : ''}, ` +
|
|
209
222
|
'controles estructurales válidos',
|
|
210
223
|
)
|
|
211
|
-
} catch (error) { fail(error.message,
|
|
224
|
+
} catch (error) { fail(error.message, USAGE) }
|
|
212
225
|
}
|
|
213
226
|
|
|
214
|
-
|
|
227
|
+
// La raíz la toma como posicional, igual que el resto de los comandos que leen una instancia: `list` no
|
|
228
|
+
// lleva recorrido, así que la suya es el primer posicional, y `check`/`show` la llevan después del
|
|
229
|
+
// recorrido —la misma forma que `agents fork <cargo> [ops-root]`—. Antes se descartaba y todo resolvía por
|
|
230
|
+
// `opsRoot()`, así que `ops flow list <raíz>` contestaba sobre el directorio actual y la respuesta se leía
|
|
231
|
+
// igual de bien viniendo de otra instancia (caso 172).
|
|
232
|
+
function flow(action, slug, dir, cli) {
|
|
233
|
+
const root = opsRoot(action === 'list' ? slug : dir)
|
|
215
234
|
if (action === 'list') {
|
|
216
|
-
const root = opsRoot()
|
|
217
235
|
const slugs = T.list(root)
|
|
236
|
+
warnUnresolved(root, 'flows', slugs.length)
|
|
218
237
|
// La misma forma que `agents list --json`, porque la consume el mismo cron. `cadence` es fija:
|
|
219
238
|
// un recorrido no tiene profesión que cambie afuera, así que su calendario no se deriva de
|
|
220
239
|
// fuentes —no tiene—; lo que decide si le toca es `pending`, las corridas que dejó sin consolidar.
|
|
@@ -238,11 +257,11 @@ function flow(action, slug, cli) {
|
|
|
238
257
|
for (const name of slugs) console.log(name)
|
|
239
258
|
return
|
|
240
259
|
}
|
|
241
|
-
if (!['check', 'show'].includes(action)) fail(`Acción de flow desconocida: ${action || '(vacía)'}`,
|
|
260
|
+
if (!['check', 'show'].includes(action)) fail(`Acción de flow desconocida: ${action || '(vacía)'}`, USAGE)
|
|
242
261
|
try {
|
|
243
|
-
const result = T.validate(
|
|
262
|
+
const result = T.validate(root, slug)
|
|
244
263
|
for (const error of result.errors) console.error(`✗ ${error}`)
|
|
245
|
-
if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1)
|
|
264
|
+
if (result.errors.length) fail(`${slug}: ${result.errors.length} error(es)`, 1, REFUSED)
|
|
246
265
|
if (action === 'show') {
|
|
247
266
|
// El manifiesto entero, para que un workflow ejecute las etapas sin que un modelo lo parsee.
|
|
248
267
|
if (cli.has('--json')) return console.log(JSON.stringify(result.manifest))
|
|
@@ -254,7 +273,7 @@ function flow(action, slug, cli) {
|
|
|
254
273
|
} else {
|
|
255
274
|
console.log(`✓ ${slug}: ${result.stages} etapa(s), ${result.agents} agente(s), contrato válido`)
|
|
256
275
|
}
|
|
257
|
-
} catch (error) { fail(error.message,
|
|
276
|
+
} catch (error) { fail(error.message, USAGE) }
|
|
258
277
|
}
|
|
259
278
|
|
|
260
279
|
module.exports = {
|
package/engine/cli/claims.js
CHANGED
|
@@ -9,25 +9,25 @@ const path = require('node:path')
|
|
|
9
9
|
const CL = require('../planning/claims')
|
|
10
10
|
const R = require('../core/repos')
|
|
11
11
|
const ST = require('../planning/state')
|
|
12
|
-
const { fail, planningRoot, TODAY } = require('./io')
|
|
12
|
+
const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
|
|
13
13
|
|
|
14
14
|
function claim(dir, slug, cli) {
|
|
15
15
|
const root = planningRoot(dir)
|
|
16
|
-
if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`',
|
|
16
|
+
if (!slug) return fail('Falta el slug. `ops claim <planning-dir> <tarea>`', USAGE)
|
|
17
17
|
const state = ST.snapshot(root)
|
|
18
18
|
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
19
|
-
if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`,
|
|
19
|
+
if (!task) return fail(`${slug} no está en BACKLOG: sólo se toma trabajo ya promovido.`, USAGE)
|
|
20
20
|
|
|
21
21
|
// No se reserva lo que todavía no se puede empezar: una tarea tomada con su dependencia en vuelo
|
|
22
22
|
// bloquea la cola sin que nadie pueda avanzarla, y el runner que la tomó se queda sin poder tomar otra.
|
|
23
23
|
const blocker = task.depends.find((dep) => !state.done.set.has(dep))
|
|
24
|
-
if (blocker) return fail(`${slug} depende de ${blocker}, que todavía no está en DONE
|
|
24
|
+
if (blocker) return fail(`${slug} depende de ${blocker}, que todavía no está en DONE.`, REFUSED)
|
|
25
25
|
|
|
26
26
|
const me = CL.owner(root)
|
|
27
27
|
const from = CL.runner()
|
|
28
28
|
if (!me) {
|
|
29
29
|
return fail('No sé quién sos. Configurá `git config user.email` o exportá CAUCE_OWNER: '
|
|
30
|
-
+ 'un reclamo anónimo no dice a quién preguntarle.',
|
|
30
|
+
+ 'un reclamo anónimo no dice a quién preguntarle.', USAGE)
|
|
31
31
|
}
|
|
32
32
|
// Un runner lleva una tarea a la vez (BR-OPS-001), y exigirlo acá hace ruidosa la única forma en que
|
|
33
33
|
// este contrato falla en silencio: dos agentes de la misma máquina compartiendo id porque nadie puso
|
|
@@ -38,7 +38,7 @@ function claim(dir, slug, cli) {
|
|
|
38
38
|
.find((one) => one.runner === from && one.slug !== slug && !state.done.set.has(one.slug))
|
|
39
39
|
if (ocupado) {
|
|
40
40
|
return fail(`este runner ya tiene ${ocupado.slug}. Cerrala o soltala primero; y si sos otro agente `
|
|
41
|
-
+ 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.')
|
|
41
|
+
+ 'en la misma máquina, exportá CAUCE_RUNNER con un valor propio.', REFUSED)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const target = CL.file(root, slug)
|
|
@@ -58,7 +58,7 @@ function claim(dir, slug, cli) {
|
|
|
58
58
|
const holder = CL.read(root).find((one) => one.slug === slug)
|
|
59
59
|
// Existía al crear y ya no está: alguien la soltó entre las dos operaciones. Es una ventana de
|
|
60
60
|
// microsegundos y aun así tiene respuesta, porque la alternativa es reventar con un TypeError.
|
|
61
|
-
if (!holder) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo
|
|
61
|
+
if (!holder) return fail(`${slug} cambió de manos mientras la pedías; volvé a intentarlo.`, REFUSED)
|
|
62
62
|
if (holder.runner === from) return console.log(`= ${slug} ya era tuya desde ${holder.started}`)
|
|
63
63
|
// Mismo dueño y otro runner son dos situaciones que se ven idénticas desde acá —vos retomando la
|
|
64
64
|
// sesión de ayer, o un segundo agente tuyo— y ninguna se puede distinguir mirando el archivo.
|
|
@@ -68,10 +68,10 @@ function claim(dir, slug, cli) {
|
|
|
68
68
|
return fail(`${slug} la tenés vos, tomada el ${holder.started} desde el runner ${holder.runner}. `
|
|
69
69
|
+ 'Preguntá si se retoma esa sesión —y entonces corré con ese id— o si es otro agente en '
|
|
70
70
|
+ `paralelo, que toma otra tarea. \`ops runners ${path.relative(process.cwd(), root) || '.'}\` `
|
|
71
|
-
+ 'lista lo que hay abierto.')
|
|
71
|
+
+ 'lista lo que hay abierto.', REFUSED)
|
|
72
72
|
}
|
|
73
73
|
return fail(`${slug} la tomó ${holder.owner} el ${holder.started}. Si se abandonó, borrá `
|
|
74
|
-
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando
|
|
74
|
+
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando.`, REFUSED)
|
|
75
75
|
}
|
|
76
76
|
console.log(`✓ ${slug} tomada por ${me}`)
|
|
77
77
|
if (!cli.has('--json')) {
|
|
@@ -87,12 +87,12 @@ function claim(dir, slug, cli) {
|
|
|
87
87
|
|
|
88
88
|
function release(dir, slug) {
|
|
89
89
|
const root = planningRoot(dir)
|
|
90
|
-
if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`',
|
|
90
|
+
if (!slug) return fail('Falta el slug. `ops release <planning-dir> <tarea>`', USAGE)
|
|
91
91
|
const from = CL.runner()
|
|
92
92
|
const taken = CL.read(root).find((one) => one.slug === slug)
|
|
93
|
-
if (!taken) return fail(`${slug} no está tomada por nadie.`,
|
|
93
|
+
if (!taken) return fail(`${slug} no está tomada por nadie.`, USAGE)
|
|
94
94
|
if (taken.runner !== from) {
|
|
95
|
-
return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano
|
|
95
|
+
return fail(`${slug} es de ${taken.owner}. Si se abandonó, borrá ${taken.at} a mano.`, REFUSED)
|
|
96
96
|
}
|
|
97
97
|
fs.rmSync(CL.file(root, slug))
|
|
98
98
|
console.log(`✓ ${slug} soltada; volvió a la cola`)
|
package/engine/cli/contract.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const fs = require('node:fs')
|
|
16
16
|
const path = require('node:path')
|
|
17
17
|
const P = require('../planning/parser')
|
|
18
|
-
const { fail, opsRoot } = require('./io')
|
|
18
|
+
const { fail, opsRoot, USAGE } = require('./io')
|
|
19
19
|
|
|
20
20
|
// Los cuatro que componen el contrato, con la ruta relativa a la raíz de la instancia. El orden es el que
|
|
21
21
|
// usa el mensaje de error: se nombra el primero que falte y no los cuatro, porque arreglar uno suele
|
|
@@ -188,14 +188,15 @@ function contract(dir, cli) {
|
|
|
188
188
|
// se pierde no se ve en la salida, se ve tres fases después en lo que un subagente creyó que podía tocar.
|
|
189
189
|
if (missing) {
|
|
190
190
|
return fail(`${root} no tiene ${missing}, así que no hay contrato que derivar. Es la raíz que escribió `
|
|
191
|
-
+ '`automation install`: comprobá que exista y, si moviste el proyecto de carpeta, reinstalá el adaptador.',
|
|
191
|
+
+ '`automation install`: comprobá que exista y, si moviste el proyecto de carpeta, reinstalá el adaptador.',
|
|
192
|
+
USAGE)
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
let config
|
|
195
196
|
try {
|
|
196
197
|
config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
197
198
|
} catch (error) {
|
|
198
|
-
return fail(`ops.config.json no se pudo leer como JSON: ${error.message}`,
|
|
199
|
+
return fail(`ops.config.json no se pudo leer como JSON: ${error.message}`, USAGE)
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
const sections = {}
|
|
@@ -205,7 +206,7 @@ function contract(dir, cli) {
|
|
|
205
206
|
// que poder abrir el archivo y ver qué encabezado falta, y el arreglo es restaurarlo con `upgrade`.
|
|
206
207
|
if (!found.trim()) {
|
|
207
208
|
return fail(`${file} no tiene la sección ${name}, y de ahí sale el contrato que reciben los agentes. `
|
|
208
|
-
+ 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.',
|
|
209
|
+
+ 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.', USAGE)
|
|
209
210
|
}
|
|
210
211
|
sections[file] = found
|
|
211
212
|
}
|
package/engine/cli/dependency.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const fs = require('node:fs')
|
|
8
8
|
const path = require('node:path')
|
|
9
9
|
const F = require('../core/files')
|
|
10
|
-
const { fail } = require('./io')
|
|
10
|
+
const { fail, USAGE } = require('./io')
|
|
11
11
|
|
|
12
12
|
// Declara el motor como dependencia exacta: el lockfile decide qué versión corre, no una copia.
|
|
13
13
|
// Conserva el manifiesto existente porque el repo anfitrión puede tener el suyo.
|
|
@@ -15,7 +15,7 @@ function declareEngine(manifest, version) {
|
|
|
15
15
|
let pkg = { name: path.basename(path.dirname(manifest)), private: true, version: '0.0.0' }
|
|
16
16
|
if (fs.existsSync(manifest)) {
|
|
17
17
|
try { pkg = JSON.parse(fs.readFileSync(manifest, 'utf8')) } catch (error) {
|
|
18
|
-
fail(`package.json inválido en ${manifest}: ${error.message}
|
|
18
|
+
fail(`package.json inválido en ${manifest}: ${error.message}`, USAGE)
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
pkg.devDependencies = { ...pkg.devDependencies, '@ingeniomaps/cauce': version }
|
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, TODAY } = require('./io')
|
|
18
|
+
const { fail, TODAY, USAGE, REFUSED } = require('./io')
|
|
19
19
|
const { declareEngine, pinEngine, undeclareEngine } = require('./dependency')
|
|
20
20
|
const { adviceFor, previewUpgrade, reportUpgrade } = require('./upgrade-report')
|
|
21
21
|
|
|
@@ -47,7 +47,7 @@ function copyTemplate(source, target, replacements, force, skip = [], quiet = fa
|
|
|
47
47
|
if (entry.isDirectory()) Object.assign(preserved, copyTemplate(from, to, replacements, force, skip, quiet))
|
|
48
48
|
else {
|
|
49
49
|
if (fs.existsSync(to)) {
|
|
50
|
-
if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force
|
|
50
|
+
if (!force) fail(`El destino contiene ${to}. Usa un directorio vacío o --force.`, REFUSED)
|
|
51
51
|
if (!quiet) console.log(`= conservado ${to}`)
|
|
52
52
|
let would = fs.readFileSync(from, 'utf8')
|
|
53
53
|
for (const [key, value] of Object.entries(replacements)) would = would.replaceAll(key, value)
|
|
@@ -175,9 +175,9 @@ function whatIsLost(root) {
|
|
|
175
175
|
function destroy(dir, cli) {
|
|
176
176
|
const root = path.resolve(dir || '.')
|
|
177
177
|
if (!fs.existsSync(path.join(root, 'ops.config.json'))) {
|
|
178
|
-
fail(`${root} no es una instancia de Cauce: falta ops.config.json.`,
|
|
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
|
|
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
|
|
@@ -200,14 +205,14 @@ async function run(cli) {
|
|
|
200
205
|
if (!command || command === 'help') return usage()
|
|
201
206
|
// `Object.hasOwn` y no `FLAGS[command]`: `constructor` heredado de `Object.prototype` pasaba por
|
|
202
207
|
// comando válido y el CLI salía con 0 sin hacer nada.
|
|
203
|
-
if (!Object.hasOwn(FLAGS, command)) { usage(); fail(`Comando desconocido: ${command}`,
|
|
208
|
+
if (!Object.hasOwn(FLAGS, command)) { usage(); fail(`Comando desconocido: ${command}`, USAGE) }
|
|
204
209
|
// `--help` valía sólo como primer argumento: `check --help` corría `check` contra el directorio
|
|
205
210
|
// actual en vez de explicarse.
|
|
206
211
|
if (cli.has('--help')) return usage()
|
|
207
212
|
const unknown = cli.unknown(command)
|
|
208
213
|
if (unknown.length) {
|
|
209
214
|
const accepts = FLAGS[command].length ? `Acepta: ${FLAGS[command].join(', ')}.` : 'No acepta banderas.'
|
|
210
|
-
fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`,
|
|
215
|
+
fail(`${command}: bandera desconocida ${unknown.join(', ')}. ${accepts}`, USAGE)
|
|
211
216
|
}
|
|
212
217
|
const arg = cli.positional
|
|
213
218
|
if (command === 'init') await init(arg[1], cli)
|
|
@@ -236,7 +241,7 @@ async function run(cli) {
|
|
|
236
241
|
else if (command === 'automation') W.automation(arg[1], arg[2], arg[3], cli)
|
|
237
242
|
else if (command === 'learn') CAT.learn(arg[1], cli)
|
|
238
243
|
else if (command === 'evaluate') CAT.evaluate(arg[1], arg[2], cli)
|
|
239
|
-
else if (command === 'flow') CAT.flow(arg[1], arg[2], cli)
|
|
244
|
+
else if (command === 'flow') CAT.flow(arg[1], arg[2], arg[3], cli)
|
|
240
245
|
}
|
|
241
246
|
|
|
242
|
-
run(parse(process.argv.slice(2))).catch((error) => fail(error.message))
|
|
247
|
+
run(parse(process.argv.slice(2))).catch((error) => fail(error.message, REFUSED))
|
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 */ }
|
|
@@ -148,7 +148,7 @@ function context(dir, cli) {
|
|
|
148
148
|
// Un hito mal escrito devolvería «sin tarea disponible», que es indistinguible de un hito terminado.
|
|
149
149
|
if (!existe) {
|
|
150
150
|
const hay = state.milestones.map((one) => one.slug).join(', ') || '(ninguno)'
|
|
151
|
-
return fail(`el hito ${hito} no existe. Hay: ${hay}`,
|
|
151
|
+
return fail(`el hito ${hito} no existe. Hay: ${hay}`, USAGE)
|
|
152
152
|
}
|
|
153
153
|
if (own) hitoOmitido = `${hito} no se aplica: ya tenés ${own.slug} tomada`
|
|
154
154
|
else state.milestones = state.milestones.filter((one) => one.slug === hito)
|
|
@@ -299,7 +299,7 @@ function recurring(dir, cli) {
|
|
|
299
299
|
const promote = cli.value('--promote')
|
|
300
300
|
if (promote) {
|
|
301
301
|
const one = state.find((candidate) => candidate.id === promote)
|
|
302
|
-
if (!one) return fail(`${RC.FILE} no declara ${promote}`,
|
|
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:`)
|
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)
|
|
@@ -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
|
|
package/engine/cli/worktree.js
CHANGED
|
@@ -15,7 +15,7 @@ const ST = require('../planning/state')
|
|
|
15
15
|
const CL = require('../planning/claims')
|
|
16
16
|
const R = require('../core/repos')
|
|
17
17
|
const O = require('../core/ownership')
|
|
18
|
-
const { fail, planningRoot } = require('./io')
|
|
18
|
+
const { fail, planningRoot, USAGE, REFUSED } = require('./io')
|
|
19
19
|
|
|
20
20
|
const git = (cwd, ...args) => spawnSync('git', args, { cwd, encoding: 'utf8' })
|
|
21
21
|
|
|
@@ -34,38 +34,38 @@ function existing(repo, branch) {
|
|
|
34
34
|
|
|
35
35
|
function worktree(dir, slug, cli) {
|
|
36
36
|
const root = planningRoot(dir)
|
|
37
|
-
if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`',
|
|
37
|
+
if (!slug) return fail('Falta el slug. `ops worktree <planning-dir> <tarea>`', USAGE)
|
|
38
38
|
const state = ST.snapshot(root)
|
|
39
39
|
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
40
|
-
if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`,
|
|
40
|
+
if (!task) return fail(`${slug} no está en BACKLOG: sólo se prepara trabajo ya promovido.`, USAGE)
|
|
41
41
|
|
|
42
42
|
// No reserva —eso es `claim`— pero se niega a montar sobre lo de otro: preparar un árbol para una
|
|
43
43
|
// tarea ajena es trabajo que se va a tirar, y el aviso cuesta menos que descubrirlo después.
|
|
44
44
|
const taken = state.claims.find((one) => one.slug === slug)
|
|
45
45
|
if (taken && taken.runner !== CL.runner()) {
|
|
46
|
-
return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie
|
|
46
|
+
return fail(`${slug} la tomó ${taken.owner}; preparar un árbol para su tarea no ayuda a nadie.`, REFUSED)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const candidatos = R.reposFor(path.join(root, '..'), task.service)
|
|
50
50
|
if (candidatos.length > 1) {
|
|
51
51
|
return fail(`${task.service || '.'} existe en más de un repositorio (${candidatos.join(', ')}), así que `
|
|
52
|
-
+ 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.',
|
|
52
|
+
+ 'no puedo saber cuál. Escribí un `service:` que sólo exista en uno.', USAGE)
|
|
53
53
|
}
|
|
54
54
|
const repo = candidatos[0]
|
|
55
55
|
if (!repo) {
|
|
56
56
|
return fail(`no encontré el repositorio de ${task.service || '(sin service)'}: revisá workspaceRoots `
|
|
57
|
-
+ 'en ops.config.json y que la ruta del servicio exista.',
|
|
57
|
+
+ 'en ops.config.json y que la ruta del servicio exista.', USAGE)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const branch = CL.branchOf(slug)
|
|
61
61
|
const already = existing(repo, branch)
|
|
62
62
|
const target = already || path.join(path.dirname(repo), `${path.basename(repo)}-${slug}`)
|
|
63
63
|
if (!already) {
|
|
64
|
-
if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama
|
|
64
|
+
if (fs.existsSync(target)) return fail(`${target} ya existe y no es un árbol de esta rama.`, REFUSED)
|
|
65
65
|
const hasBranch = git(repo, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`).status === 0
|
|
66
66
|
const args = hasBranch ? ['worktree', 'add', target, branch] : ['worktree', 'add', '-b', branch, target]
|
|
67
67
|
const added = git(repo, ...args)
|
|
68
|
-
if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}
|
|
68
|
+
if (added.status !== 0) return fail(`git worktree add falló: ${(added.stderr || '').trim()}`, REFUSED)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (cli.has('--json')) {
|
package/engine/core/ownership.js
CHANGED
|
@@ -164,9 +164,16 @@ function engineAt(root, relative = '') {
|
|
|
164
164
|
// de copiarse. Se reconocen por contener `system/`, que es el espacio del toolkit y no algo que un
|
|
165
165
|
// proyecto deba crear. Una sola implementación para las dos, o divergen.
|
|
166
166
|
function packageDir(root, name) {
|
|
167
|
+
// La raíz declarada va también acá, y no sólo en `packagePath`. Las dos contestan dónde está el paquete
|
|
168
|
+
// sobre la misma instancia y desde el 158 contestaban distinto: en el layout documentado —`npm install`
|
|
169
|
+
// en la carpeta de la empresa y la instancia adentro— el motor se resolvía y el catálogo no, así que
|
|
170
|
+
// `check` pasaba en verde y `agents list` devolvía una lista vacía con exit 0, indistinguible de «esta
|
|
171
|
+
// instancia no tiene cargos». `evaluate` era peor: culpaba a un `SKILL.md` que falta (caso 171).
|
|
172
|
+
const above = declaredRoot(root)
|
|
167
173
|
const candidates = [
|
|
168
174
|
path.join(root, 'node_modules', '@ingeniomaps', 'cauce', name),
|
|
169
175
|
path.join(root, name),
|
|
176
|
+
...(above ? [path.join(above, 'node_modules', '@ingeniomaps', 'cauce', name)] : []),
|
|
170
177
|
]
|
|
171
178
|
return candidates.find((dir) => fs.existsSync(path.join(dir, 'system'))
|
|
172
179
|
|| fs.existsSync(path.join(dir, 'roles', 'system'))) || ''
|