@ingeniomaps/cauce 0.94.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +238 -0
- package/automatization/workflows/autobuild.js +28 -1
- package/engine/agents/evaluations.js +20 -5
- package/engine/agents/fork.js +21 -8
- package/engine/agents/learning-sources.js +7 -7
- package/engine/agents/learning.js +15 -6
- package/engine/automation/check.js +8 -2
- package/engine/automation/config.js +9 -9
- package/engine/automation/index.js +19 -8
- package/engine/cli/archive.js +3 -6
- package/engine/cli/args.js +16 -2
- package/engine/cli/bench.js +9 -9
- package/engine/cli/catalog.js +36 -17
- package/engine/cli/claims.js +30 -32
- package/engine/cli/contract.js +29 -11
- package/engine/cli/dependency.js +2 -2
- package/engine/cli/instance.js +25 -24
- package/engine/cli/io.js +26 -4
- package/engine/cli/ops.js +24 -15
- package/engine/cli/planning.js +8 -8
- package/engine/cli/upgrade-report.js +10 -10
- package/engine/cli/validate.js +9 -9
- package/engine/cli/wiring.js +16 -16
- package/engine/cli/worktree.js +8 -8
- package/engine/core/onboarding.js +4 -1
- package/engine/core/ownership.js +9 -2
- package/engine/core/repos.js +18 -12
- package/engine/hooks/approval.js +9 -1
- package/engine/hooks/chat.js +27 -5
- package/engine/hooks/files.js +6 -6
- package/engine/hooks/input.js +5 -2
- package/engine/hooks/run.js +3 -3
- package/engine/hooks/shell.js +20 -6
- package/engine/hooks/verify.js +7 -7
- package/engine/integrations/proposals.js +5 -1
- package/engine/integrations/registry.js +7 -0
- package/engine/integrations/state.js +6 -3
- package/engine/planning/adoption.js +2 -2
- package/engine/planning/claims.js +8 -8
- package/engine/planning/parser.js +8 -8
- package/engine/planning/state.js +2 -2
- package/engine/planning/structure.js +7 -7
- package/package.json +1 -1
package/engine/cli/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
|
@@ -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
|
|
@@ -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.
|
|
@@ -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 } = 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
|
-
const date =
|
|
23
|
-
try { result = require('../agents/fork').fork(root, slug, date) } catch (error) { fail(error.message,
|
|
22
|
+
const date = TODAY()
|
|
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.
|
|
@@ -179,7 +192,7 @@ function evaluate(agent, caso, cli) {
|
|
|
179
192
|
// Dónde escribir el registro de esta corrida. Lo pregunta el recorrido en vez de componer el
|
|
180
193
|
// nombre, que es lo que hacía que la segunda corrida de un día borrara a la primera.
|
|
181
194
|
if (cli.has('--record')) {
|
|
182
|
-
const day = cli.value('--record') ||
|
|
195
|
+
const day = cli.value('--record') || TODAY()
|
|
183
196
|
return console.log(path.relative(root,
|
|
184
197
|
path.join(EV.resultsDir(root, agent, kind), EV.nextResult(root, agent, day, kind))))
|
|
185
198
|
}
|
|
@@ -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,27 +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 } = require('./io')
|
|
13
|
-
|
|
14
|
-
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
12
|
+
const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
|
|
15
13
|
|
|
16
14
|
function claim(dir, slug, cli) {
|
|
17
15
|
const root = planningRoot(dir)
|
|
18
|
-
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)
|
|
19
17
|
const state = ST.snapshot(root)
|
|
20
18
|
const task = state.milestones.flatMap((milestone) => milestone.tasks).find((one) => one.slug === slug)
|
|
21
|
-
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)
|
|
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.`, REFUSED)
|
|
27
25
|
|
|
28
26
|
const me = CL.owner(root)
|
|
29
27
|
const from = CL.runner()
|
|
30
28
|
if (!me) {
|
|
31
29
|
return fail('No sé quién sos. Configurá `git config user.email` o exportá CAUCE_OWNER: '
|
|
32
|
-
+ 'un reclamo anónimo no dice a quién preguntarle.',
|
|
30
|
+
+ 'un reclamo anónimo no dice a quién preguntarle.', USAGE)
|
|
33
31
|
}
|
|
34
32
|
// Un runner lleva una tarea a la vez (BR-OPS-001), y exigirlo acá hace ruidosa la única forma en que
|
|
35
33
|
// este contrato falla en silencio: dos agentes de la misma máquina compartiendo id porque nadie puso
|
|
@@ -40,12 +38,12 @@ function claim(dir, slug, cli) {
|
|
|
40
38
|
.find((one) => one.runner === from && one.slug !== slug && !state.done.set.has(one.slug))
|
|
41
39
|
if (ocupado) {
|
|
42
40
|
return fail(`este runner ya tiene ${ocupado.slug}. Cerrala o soltala primero; y si sos otro agente `
|
|
43
|
-
+ '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)
|
|
44
42
|
}
|
|
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,26 +52,26 @@ 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.`, REFUSED)
|
|
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
|
-
+ 'lista lo que hay abierto.')
|
|
71
|
+
+ 'lista lo que hay abierto.', REFUSED)
|
|
74
72
|
}
|
|
75
|
-
return fail(`${slug} la tomó ${
|
|
76
|
-
+ `${CL.DIR}/${slug}.md a mano: soltar lo de otro es una decisión, no un comando
|
|
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.`, REFUSED)
|
|
77
75
|
}
|
|
78
76
|
console.log(`✓ ${slug} tomada por ${me}`)
|
|
79
77
|
if (!cli.has('--json')) {
|
|
@@ -89,12 +87,12 @@ function claim(dir, slug, cli) {
|
|
|
89
87
|
|
|
90
88
|
function release(dir, slug) {
|
|
91
89
|
const root = planningRoot(dir)
|
|
92
|
-
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)
|
|
93
91
|
const from = CL.runner()
|
|
94
92
|
const taken = CL.read(root).find((one) => one.slug === slug)
|
|
95
|
-
if (!taken) return fail(`${slug} no está tomada por nadie.`,
|
|
93
|
+
if (!taken) return fail(`${slug} no está tomada por nadie.`, USAGE)
|
|
96
94
|
if (taken.runner !== from) {
|
|
97
|
-
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)
|
|
98
96
|
}
|
|
99
97
|
fs.rmSync(CL.file(root, slug))
|
|
100
98
|
console.log(`✓ ${slug} soltada; volvió a la cola`)
|
|
@@ -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
|
@@ -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
|
|
@@ -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')
|
|
@@ -171,14 +188,15 @@ function contract(dir, cli) {
|
|
|
171
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.
|
|
172
189
|
if (missing) {
|
|
173
190
|
return fail(`${root} no tiene ${missing}, así que no hay contrato que derivar. Es la raíz que escribió `
|
|
174
|
-
+ '`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)
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
let config
|
|
178
196
|
try {
|
|
179
197
|
config = JSON.parse(fs.readFileSync(path.join(root, 'ops.config.json'), 'utf8'))
|
|
180
198
|
} catch (error) {
|
|
181
|
-
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)
|
|
182
200
|
}
|
|
183
201
|
|
|
184
202
|
const sections = {}
|
|
@@ -188,7 +206,7 @@ function contract(dir, cli) {
|
|
|
188
206
|
// que poder abrir el archivo y ver qué encabezado falta, y el arreglo es restaurarlo con `upgrade`.
|
|
189
207
|
if (!found.trim()) {
|
|
190
208
|
return fail(`${file} no tiene la sección ${name}, y de ahí sale el contrato que reciben los agentes. `
|
|
191
|
-
+ 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.',
|
|
209
|
+
+ 'Ese archivo lo reemplaza `ops upgrade` entero: corrélo para restaurarlo.', USAGE)
|
|
192
210
|
}
|
|
193
211
|
sections[file] = found
|
|
194
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 }
|