@ingeniomaps/cauce 0.77.0 → 0.79.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 +74 -0
- package/automatization/workflows/autobuild.js +55 -12
- package/engine/hooks/files.js +32 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,80 @@ 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.79.0] - 2026-09-10
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **La fila que registra una parada se escribe antes de parar.** Cuando ningún plan sobrevive a la
|
|
22
|
+
crítica, el recorrido pide que la tarea quede en `HUMAN_ACTIONS.md` con su motivo —así `context` deja
|
|
23
|
+
de ofrecerla y alguien la ve—. Esa escritura se lanzaba y el recorrido volvía en la línea siguiente,
|
|
24
|
+
así que el agente que la escribía se quedaba a mitad de camino: en una corrida real el resumen contó
|
|
25
|
+
diez agentes y el registro nueve. La parada se informaba bien y el disco no la tenía.
|
|
26
|
+
|
|
27
|
+
Ahora se espera. Y en las tres paradas que dejan fila —plan rechazado, tarea que no está lista,
|
|
28
|
+
criterio que no dice qué aserciar— si el agente no contesta, el detalle de la parada lo dice: el
|
|
29
|
+
motivo sigue siendo el que la causó, y se agrega que la fila hay que escribirla a mano.
|
|
30
|
+
|
|
31
|
+
**Qué cambia para vos**: relanzar después de un plan rechazado deja de repetir la planificación
|
|
32
|
+
entera, porque la tarea queda registrada. Si venías viendo que `context` te ofrecía otra vez la tarea
|
|
33
|
+
que acababa de rechazarse, era esto.
|
|
34
|
+
|
|
35
|
+
- **El guard de migraciones frena por haber viajado, no por estar en disco.** Escribir una migración son
|
|
36
|
+
dos pasos —crearla y completarla— y el segundo se bloqueaba: el chequeo preguntaba si el archivo
|
|
37
|
+
existía, que no es la pregunta. Un stub de hace dos segundos y una migración publicada hace un año
|
|
38
|
+
daban la misma respuesta, y **ninguna herramienta lo esquivaba** —`Write`, `Edit` y `MultiEdit` se
|
|
39
|
+
bloqueaban igual—, así que lo único que quedaba a la vista era apagar el guard entero, incluida la
|
|
40
|
+
protección contra SQL destructivo.
|
|
41
|
+
|
|
42
|
+
Ahora se le pregunta a git si el archivo está en `HEAD`. En el índice no alcanza: un archivo apenas
|
|
43
|
+
agregado no viajó a ninguna parte. Y sin repositorio con el que contestar —un proyecto sin git— se
|
|
44
|
+
conserva la conducta anterior en vez de dejar pasar, que es el lado correcto para equivocarse cuando
|
|
45
|
+
no se puede saber.
|
|
46
|
+
|
|
47
|
+
**Qué cambia para vos**: completar una migración recién creada deja de frenarse. El mensaje del
|
|
48
|
+
bloqueo pasa a decir el hecho que lo sostiene —que está en el historial, o que existe y acá no hay con
|
|
49
|
+
qué saberlo— y a nombrar cómo aprobar esa ruta puntual en `planning/.ops-approval`, que es lo que el
|
|
50
|
+
bloqueo hermano ya hacía. Si tenías `OPS_MIGRATIONS_OVERRIDE=1` puesto para poder trabajar, ya no hace
|
|
51
|
+
falta y conviene sacarlo: apagaba también lo que sí te cuida.
|
|
52
|
+
|
|
53
|
+
## [0.78.0] - 2026-09-10
|
|
54
|
+
|
|
55
|
+
### Corregido
|
|
56
|
+
|
|
57
|
+
- **Las puertas que revisan un recorrido leen los literales de regex.** Tres pruebas analizan cada
|
|
58
|
+
workflow como texto —que su `meta` sea un literal puro, que no llame a nada inexistente y que no use un
|
|
59
|
+
nombre que no declaró— y ninguna sabía qué es un regex. La barra escapada que cierra uno deja un `//`
|
|
60
|
+
literal, que se leía como el arranque de un comentario: a partir de ahí se perdía el resto de la línea,
|
|
61
|
+
en silencio. Dos recorridos del catálogo tenían cuatro líneas así.
|
|
62
|
+
|
|
63
|
+
**Qué cambia para vos**: nada en lo que recibís, y sí en lo que se puede confiar. Un recorrido propio
|
|
64
|
+
con un regex adentro ahora se revisa entero en vez de a medias, y un `meta` con un regex o con una
|
|
65
|
+
interpolación —que hasta acá pasaba siempre, porque esa regla no podía dispararse— se rechaza.
|
|
66
|
+
- **El nombre de un banco de pruebas no trae nada que la prueba no haya pedido.** La suite aísla lo que
|
|
67
|
+
cada prueba mide filtrando la salida de `check`, y esa salida empieza con la ruta del banco: cuando el
|
|
68
|
+
sufijo aleatorio del directorio terminó en `adr`, un error ajeno entró a una prueba de plantillas de
|
|
69
|
+
ADR y la puso en rojo. Falla una vez cada muchas corridas y se lee como un hipo del entorno.
|
|
70
|
+
|
|
71
|
+
**Qué cambia para vos**: nada — es la suite del toolkit, no algo que recibas. Va acá porque una puerta
|
|
72
|
+
que falla al azar enseña a relanzar en vez de a mirar, y eso sí llega a quien la usa.
|
|
73
|
+
|
|
74
|
+
- **`autobuild` lee `blocked` por su valor y no por su verdad.** El campo del contexto es un vocabulario
|
|
75
|
+
de tres valores —vacío, `awaiting-review` y `blocked-on-human`—, y el recorrido lo probaba con un `if`
|
|
76
|
+
a secas. Dos consecuencias, y la que más costaba no era intermitente: una cola trabada por acciones
|
|
77
|
+
humanas frenaba con el motivo del checkpoint de hito y mandaba a mirar `AWAITING_REVIEW.md`, un archivo
|
|
78
|
+
que en ese escenario no existe. Ahora cada bloqueo para con su motivo y nombra su archivo:
|
|
79
|
+
`AWAITING_REVIEW.md` para el checkpoint, `HUMAN_ACTIONS.md` —con las tareas trabadas— para la cola.
|
|
80
|
+
|
|
81
|
+
La otra es la que se ve al azar: el agente que transcribe el contexto a veces entrega el vacío como la
|
|
82
|
+
cadena de dos comillas, y eso frenaba la corrida en la fase 1 sin que hubiera nada que resolver. Medido
|
|
83
|
+
sobre siete corridas de una instancia real: 3 de 24 lecturas llegaron así, y una de cada siete cayó en
|
|
84
|
+
la única lectura que consulta el campo. Las formas equivocadas del vacío se desenvuelven antes de leer.
|
|
85
|
+
|
|
86
|
+
Y lo que no está en el vocabulario ya no se adivina en ninguna de las dos direcciones: no se sigue como
|
|
87
|
+
si no hubiera bloqueo ni se inventa cuál es — para diciendo que el contexto llegó fuera del
|
|
88
|
+
vocabulario. El esquema declara los tres valores, igual que `lane`; eso documenta el contrato, y lo que
|
|
89
|
+
sostiene el arreglo es la lectura.
|
|
90
|
+
|
|
17
91
|
## [0.77.0] - 2026-09-10
|
|
18
92
|
|
|
19
93
|
### Corregido
|
|
@@ -38,6 +38,11 @@ const GATE = `${P}/AWAITING_REVIEW.md`
|
|
|
38
38
|
const ROADMAP = `${P}/roadmap`
|
|
39
39
|
|
|
40
40
|
// Estado de planning tal como lo emite `ops context --json`; ningún modelo parsea BACKLOG ni WIP.
|
|
41
|
+
// De a pares, y sin regex: una comilla dentro de un literal de regex desincroniza a las dos puertas que
|
|
42
|
+
// leen este archivo sin parsearlo —caso 084—, y de a pares es además lo correcto, porque una comilla
|
|
43
|
+
// suelta no es un envoltorio. Por eso también la escapada en vez de alternar el estilo de comillas.
|
|
44
|
+
const QUOTES = ['\'', '"']
|
|
45
|
+
|
|
41
46
|
const CONTEXT = {
|
|
42
47
|
type: 'object', additionalProperties: false,
|
|
43
48
|
required: ['blocked', 'hasTask', 'wipActive', 'queued', 'cast', 'readOk'],
|
|
@@ -46,7 +51,13 @@ const CONTEXT = {
|
|
|
46
51
|
// exactamente lo que un modelo completa cuando no tiene qué poner. Sin este campo esa invención se
|
|
47
52
|
// lee igual que una cola terminada, y Pick la toma como permiso para promover.
|
|
48
53
|
readOk: { type: 'boolean' },
|
|
49
|
-
|
|
54
|
+
// Vocabulario cerrado, igual que `lane` acá abajo, y por la misma razón: el motor emite tres valores
|
|
55
|
+
// y nada más —`ops context` los decide con un `existsSync` y un conteo—, así que dejarlo como texto
|
|
56
|
+
// libre le pedía a quien lo transcribe que acertara una convención invisible. Un modelo que rellena
|
|
57
|
+
// «el valor vacío» puede escribir la cadena vacía o **escribir las comillas**, y las dos satisfacían
|
|
58
|
+
// el esquema: medido en una instancia real, 3 de 24 lecturas llegaron como `"\"\""` (caso 083).
|
|
59
|
+
blocked: { type: 'string', enum: ['', 'awaiting-review', 'blocked-on-human'] },
|
|
60
|
+
hasTask: { type: 'boolean' }, wipActive: { type: 'boolean' },
|
|
50
61
|
queued: { type: 'integer' }, slug: { type: 'string' }, hito: { type: 'string' },
|
|
51
62
|
service: { type: 'string' }, acceptance: { type: 'string' }, epic: { type: 'string' },
|
|
52
63
|
// Sin declararlo acá no llega: `additionalProperties: false` lo descartaría, y el aplanado de la
|
|
@@ -344,6 +355,14 @@ const read = (prompt, options = {}) => agent(`${BASE}\n\n${prompt}`, options)
|
|
|
344
355
|
const run = (prompt, options = {}) => agent(`${SCOPE}\n\n${prompt}`, options)
|
|
345
356
|
const write = (prompt, options = {}) => agent(`${LEDGER}\n\n${prompt}`, options)
|
|
346
357
|
|
|
358
|
+
// Las tres paradas que dejan una fila en HUMAN_ACTIONS delegan esa escritura a un agente, y esa fila es
|
|
359
|
+
// el único rastro de la parada: sin ella el recorrido informa un estado que el disco no tiene. Por eso
|
|
360
|
+
// se espera —lanzarla y volver en la línea siguiente la abandona— y por eso se mira si contestó.
|
|
361
|
+
// Devuelve lo que hay que agregarle al detalle, vacío cuando la fila quedó pedida. Caso 087.
|
|
362
|
+
const registerHuman = async (prompt, label) => (await write(prompt, { label })
|
|
363
|
+
? ''
|
|
364
|
+
: ` — la fila en ${HUMAN} no se pudo registrar: escribila a mano`)
|
|
365
|
+
|
|
347
366
|
// Gate, mutex de WIP y selección de tarea salen de un comando determinista: AWAITING_REVIEW, BACKLOG,
|
|
348
367
|
// WIP y HUMAN_ACTIONS nunca entran al contexto de un modelo, y su tamaño deja de costar tokens.
|
|
349
368
|
const readContext = () => read(
|
|
@@ -364,7 +383,28 @@ if (!planning) return stop('context-unavailable', `no se pudo leer el estado de
|
|
|
364
383
|
// Que el agente conteste no significa que haya leído: el schema se completa igual con ceros. Parar acá
|
|
365
384
|
// cuesta una corrida; seguir sobre una lectura fallida escribe en el BACKLOG, y eso no se revierte solo.
|
|
366
385
|
if (!planning.readOk) return stop('context-unavailable', `${P} no se pudo leer; revisá la ruta y el cwd`)
|
|
367
|
-
|
|
386
|
+
// `blocked` se lee por su valor y no por su verdad. Como verdad, **cualquier** cadena no vacía frenaba la
|
|
387
|
+
// corrida con el mismo motivo, y eso mentía dos veces: con `blocked-on-human` —la cola trabada por
|
|
388
|
+
// acciones humanas, que es otra cosa— mandaba a mirar un gate que no existe, y eso no era intermitente;
|
|
389
|
+
// y con la cadena `""` que a veces llega del transcriptor, frenaba sin que hubiera nada que resolver.
|
|
390
|
+
//
|
|
391
|
+
// El vacío se normaliza antes porque sus formas equivocadas no son ambiguas: dos comillas o unos espacios
|
|
392
|
+
// no son ningún bloqueo legítimo. Lo que no se reconoce **no se adivina**: para diciendo que el contexto
|
|
393
|
+
// llegó fuera del vocabulario, que es lo que esta familia de casos —056, 074, 075— pide para lo que no se
|
|
394
|
+
// pudo determinar.
|
|
395
|
+
let blocker = String(planning.blocked || '').trim()
|
|
396
|
+
while (blocker.length > 1 && QUOTES.includes(blocker[0]) && blocker[blocker.length - 1] === blocker[0]) {
|
|
397
|
+
blocker = blocker.slice(1, -1).trim()
|
|
398
|
+
}
|
|
399
|
+
if (blocker === 'awaiting-review') {
|
|
400
|
+
return stop('awaiting-human-review', `${GATE} tiene un checkpoint humano sin resolver`)
|
|
401
|
+
}
|
|
402
|
+
if (blocker === 'blocked-on-human') {
|
|
403
|
+
return stop('blocked-on-human', `toda la cola espera una acción humana. Está en ${HUMAN}`
|
|
404
|
+
+ `${(planning.blockedTasks || []).length ? `, sobre ${planning.blockedTasks.join(', ')}` : ''}`)
|
|
405
|
+
}
|
|
406
|
+
if (blocker) return stop('context-unavailable', `${P} contestó blocked=${JSON.stringify(planning.blocked)}, `
|
|
407
|
+
+ 'que no es del vocabulario. No se sabe si hay bloqueo, así que no se sigue como si no lo hubiera.')
|
|
368
408
|
|
|
369
409
|
let currentMilestone = planning.wipActive ? planning.hito : ''
|
|
370
410
|
const completed = []
|
|
@@ -520,12 +560,13 @@ while (rounds++ < MAX_TASKS) {
|
|
|
520
560
|
//
|
|
521
561
|
// Lo que la fila le pide a una persona lo dice R17: dos rechazos sobre lo mismo son el disparador
|
|
522
562
|
// posterior de división. No se parte acá porque partir es una decisión, y ésa no le toca al recorrido.
|
|
523
|
-
const planRejected = (reason, unit, found) => {
|
|
563
|
+
const planRejected = async (reason, unit, found) => {
|
|
524
564
|
const detail = found.join('; ') || 'sin condiciones nombradas'
|
|
525
|
-
|
|
565
|
+
const nota = await registerHuman(
|
|
566
|
+
`Registrá ${unit.id} en ${HUMAN}: nadie pudo escribir un plan que sobreviva a la crítica. `
|
|
526
567
|
+ `Motivo: ${detail}. La acción humana es revisar si la unidad son dos resultados con vidas `
|
|
527
|
-
+ `distintas y partirla —R17—, o dejarla entera con la razón escrita.`,
|
|
528
|
-
return stop(reason, detail)
|
|
568
|
+
+ `distintas y partirla —R17—, o dejarla entera con la razón escrita.`, 'plan-human')
|
|
569
|
+
return stop(reason, `${detail}${nota}`)
|
|
529
570
|
}
|
|
530
571
|
|
|
531
572
|
if (!planning.wipActive) {
|
|
@@ -539,9 +580,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
539
580
|
)
|
|
540
581
|
if (!ready) return stop('agent-unavailable', 'Ready no devolvió resultado')
|
|
541
582
|
if (!ready.ready) {
|
|
542
|
-
|
|
543
|
-
{
|
|
544
|
-
|
|
583
|
+
const nota = await registerHuman(
|
|
584
|
+
`Registrá ${task.id} en ${HUMAN} con el motivo y una acción humana exacta: ${ready.reason}.`,
|
|
585
|
+
'ready-human')
|
|
586
|
+
return stop('not-ready', `${ready.reason}${nota}`)
|
|
545
587
|
}
|
|
546
588
|
if (ready.refinedAcceptance) task.acceptance = ready.refinedAcceptance
|
|
547
589
|
}
|
|
@@ -761,9 +803,10 @@ while (rounds++ < MAX_TASKS) {
|
|
|
761
803
|
// hacer parar a una persona por eso le cobra una interrupción por algo que se resolvía solo.
|
|
762
804
|
const ambiguous = verified.uncovered.find((entry) => entry.cause === 'ambiguous')
|
|
763
805
|
if (ambiguous) {
|
|
764
|
-
|
|
765
|
-
`
|
|
766
|
-
|
|
806
|
+
const nota = await registerHuman(
|
|
807
|
+
`Registrá ${task.id} en ${HUMAN}: el criterio "${ambiguous.criterion}" no dice qué habría ` +
|
|
808
|
+
`que aserciar, y hace falta la decisión que lo fija.`, 'verify-human')
|
|
809
|
+
return stop('acceptance-ambiguous', `${ambiguous.criterion}${nota}`)
|
|
767
810
|
}
|
|
768
811
|
if (verified.uncovered.length) {
|
|
769
812
|
await run(`${asRole(cast.build)}Escribí sólo las pruebas que faltan en ${task.id}, con el mismo rojo ` +
|
package/engine/hooks/files.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('node:fs')
|
|
8
8
|
const path = require('node:path')
|
|
9
|
+
const { spawnSync } = require('node:child_process')
|
|
9
10
|
const {
|
|
10
11
|
patchOf, filesOf, contentOf, cwdOf, block, configOf, findOpsRoot,
|
|
11
12
|
writableRoots, outsideRoots, DECLARE_IT,
|
|
@@ -26,6 +27,29 @@ function opsRoot(input) {
|
|
|
26
27
|
// el guard hasta que cierre la sesión.
|
|
27
28
|
const approved = (input, file) => !AP.pending(opsRoot(input), [file]).length
|
|
28
29
|
|
|
30
|
+
// Si la migración ya viajó a otra copia, que es lo que el bloqueo de abajo quiere saber y `existsSync`
|
|
31
|
+
// no contesta. Devuelve el motivo del bloqueo o cadena vacía.
|
|
32
|
+
//
|
|
33
|
+
// **`HEAD` y no el índice**: un archivo apenas `git add`eado no viajó a ninguna parte, y `git ls-files`
|
|
34
|
+
// lo daría por historial. Y **resolver la raíz es una pregunta aparte** de si el archivo está en `HEAD`:
|
|
35
|
+
// las dos fallan con 128 y confundirlas repite el error que este caso arregla —decidir por la respuesta
|
|
36
|
+
// equivocada—. Sin raíz resoluble se degrada a la conducta de antes, que bloquea de más, porque cuando
|
|
37
|
+
// no se puede saber ése es el lado correcto para equivocarse. Es la degradación que `check` ya declara
|
|
38
|
+
// cuando no puede resolver el repositorio de un servicio. Caso 086.
|
|
39
|
+
function alreadyShipped(file) {
|
|
40
|
+
if (!fs.existsSync(file)) return ''
|
|
41
|
+
const cwd = path.dirname(file)
|
|
42
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
|
|
43
|
+
if (top.status !== 0) {
|
|
44
|
+
return 'existe, y acá no hay repositorio con el que saber si ya viajó a otra copia'
|
|
45
|
+
}
|
|
46
|
+
const rel = path.relative(top.stdout.trim(), file).split(path.sep).join('/')
|
|
47
|
+
return spawnSync('git', ['cat-file', '-e', `HEAD:${rel}`], { cwd }).status === 0
|
|
48
|
+
? 'ya está en el historial del repositorio'
|
|
49
|
+
: ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
29
53
|
function secrets(input) {
|
|
30
54
|
for (const file of filesOf(input)) {
|
|
31
55
|
const base = path.basename(file)
|
|
@@ -232,9 +256,15 @@ function migrations(input) {
|
|
|
232
256
|
if (destructiveSql.test(contentOf(input))) {
|
|
233
257
|
block(`${raw} contiene SQL destructivo.\n${AP.HOW('OPS_MIGRATIONS_OVERRIDE')}`)
|
|
234
258
|
}
|
|
259
|
+
// El mensaje nombra el hecho que sostiene el bloqueo y no su interpretación: «historial» era una
|
|
260
|
+
// lectura que `existsSync` no podía dar, y se la daba igual sobre stubs de la misma sesión. Y lleva
|
|
261
|
+
// la salida angosta, que hasta 0.79.0 sólo tenía el bloqueo hermano: éste es el que aparece en el
|
|
262
|
+
// flujo normal de escribir una migración, así que era justo el que no podía quedarse sin decirla.
|
|
235
263
|
const file = path.resolve(cwdOf(input), raw)
|
|
236
|
-
|
|
237
|
-
|
|
264
|
+
const shipped = alreadyShipped(file)
|
|
265
|
+
if (shipped) {
|
|
266
|
+
block(`${raw} ${shipped}. Crea una nueva en vez de reescribirla.\n`
|
|
267
|
+
+ AP.HOW('OPS_MIGRATIONS_OVERRIDE'))
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
}
|