@ingeniomaps/cauce 0.93.0 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +311 -0
- package/automatization/workflows/autobuild.js +28 -1
- package/engine/agents/evaluations.js +20 -5
- package/engine/agents/fork.js +21 -8
- package/engine/agents/learning-sources.js +7 -7
- package/engine/agents/learning.js +15 -6
- package/engine/automation/check.js +2 -2
- package/engine/automation/config.js +9 -9
- package/engine/automation/index.js +19 -8
- package/engine/automation/rules.js +27 -9
- package/engine/cli/archive.js +1 -4
- package/engine/cli/args.js +16 -2
- package/engine/cli/bench.js +3 -3
- package/engine/cli/catalog.js +3 -3
- package/engine/cli/claims.js +21 -23
- package/engine/cli/contract.js +24 -7
- package/engine/cli/instance.js +19 -19
- package/engine/cli/ops.js +7 -3
- package/engine/cli/planning.js +4 -4
- package/engine/cli/upgrade-report.js +10 -10
- package/engine/cli/validate.js +7 -7
- package/engine/core/onboarding.js +4 -1
- package/engine/core/ownership.js +2 -2
- package/engine/core/repos.js +18 -12
- package/engine/hooks/approval.js +9 -1
- package/engine/hooks/chat.js +27 -5
- package/engine/hooks/files.js +6 -6
- package/engine/hooks/input.js +5 -2
- package/engine/hooks/run.js +3 -3
- package/engine/hooks/shell.js +20 -6
- package/engine/hooks/verify.js +7 -7
- package/engine/integrations/proposals.js +5 -1
- package/engine/integrations/registry.js +7 -0
- package/engine/integrations/state.js +6 -3
- package/engine/planning/adoption.js +2 -2
- package/engine/planning/claims.js +8 -8
- package/engine/planning/parser.js +8 -8
- package/engine/planning/state.js +2 -2
- package/engine/planning/structure.js +7 -7
- package/package.json +1 -1
- package/template/planning/rules/README.md +8 -5
- package/template/planning/rules/system/code-shape.md +19 -0
- package/template/planning/rules/system/commits.md +35 -0
- package/template/planning/rules/system/conduct.md +23 -0
- package/template/planning/rules/system/process.md +48 -118
- package/template/planning/rules/system/runs.md +153 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,317 @@ 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.95.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **El bloqueo dice hasta dónde llega el «dale».** Ofrece dos salidas y sólo la del pegado decía su
|
|
22
|
+
alcance —«valen para ese conjunto y dejan de valer en cuanto cambie»—; la del chat, que es la que se
|
|
23
|
+
ofrece primero, decía sólo «reintentá el mismo cambio y pasa».
|
|
24
|
+
|
|
25
|
+
Faltaban **dos** cosas y no son la misma. Que el «dale» cubre lo que se frenó y nada más: por eso en una
|
|
26
|
+
carpeta donde se itera cada archivo nuevo vuelve a frenar, que desde afuera se lee como si el guard se
|
|
27
|
+
hubiera olvidado de que ya la aprobaste. Y que lo concedido **sigue valiendo en los mensajes
|
|
28
|
+
siguientes** hasta que lo niegues — la mitad permisiva, la que no se nota porque lo que no ocurre es un
|
|
29
|
+
bloqueo. `check` la muestra al final de la corrida; ahora también se dice al concederla.
|
|
30
|
+
|
|
31
|
+
No cambia ningún permiso: el diff es texto.
|
|
32
|
+
|
|
33
|
+
- **Partir una tarea de una épica actualiza también sus historias, así que `check` no queda en rojo.** Una
|
|
34
|
+
tarea que viene de una épica es además una historia suya, y la partición reemplazaba sólo su línea del
|
|
35
|
+
BACKLOG. La lista de historias quedaba nombrando un slug que ya no existe en ninguna parte, y eso rompe
|
|
36
|
+
por los dos lados según qué escriba la partición: con las subtareas declarando la épica, `check` falla en
|
|
37
|
+
el acto con «BACKLOG \<sub\>: no existe en epic-NNN»; sin declararla pasa en verde y **la épica no puede
|
|
38
|
+
cerrar nunca**, porque `closed` exige evidencia de cada historia y la original jamás la va a tener.
|
|
39
|
+
|
|
40
|
+
Ahí es donde la unidad partida se cierra diciendo en qué se partió, que es lo que pide R25. **No va una
|
|
41
|
+
entrada en `done/`**: ese directorio es la evidencia de lo que una tarea entregó, y una partida no
|
|
42
|
+
entregó nada. Una tarea sin épica no necesita ningún cierre — deja de existir limpiamente y no hay
|
|
43
|
+
cruce que se rompa.
|
|
44
|
+
|
|
45
|
+
Si tenés una épica con una historia que ninguna tarea de la cola nombra y que nunca vas a poder cerrar,
|
|
46
|
+
viene de esto: reemplazá esa historia por las de las subtareas que la partición dejó en el BACKLOG.
|
|
47
|
+
|
|
48
|
+
- **Un límite escrito en dos líneas ya viaja entero a los agentes.** Una viñeta bajo `### Límites` se
|
|
49
|
+
recorría línea por línea, así que de un límite que no entraba en el ancho del archivo —que es como se
|
|
50
|
+
escribe cualquiera de verdad— llegaba **sólo la primera línea**, cortada a mitad de frase. Y lo que se
|
|
51
|
+
pierde ahí suele ser lo que el límite decide: «…crear, editar o borrar productos, pedidos o» sin el «no
|
|
52
|
+
las hace un runner» que venía abajo.
|
|
53
|
+
|
|
54
|
+
La continuación, además, volvía por el otro lado: no coincidía con ninguna viñeta declarada y `check` la
|
|
55
|
+
reportaba como un párrafo «que no llega a los agentes», citando media frase tuya. O sea que el proyecto
|
|
56
|
+
hacía lo que el aviso pedía, el aviso seguía sonando, y el límite que sí viajaba era el mutilado.
|
|
57
|
+
|
|
58
|
+
Si escribiste tus límites en una sola línea para que el aviso bajara a cero, ya podés volver a partirlos
|
|
59
|
+
donde corresponda. Una línea en blanco cierra la viñeta, así que la prosa que venga después sigue sin
|
|
60
|
+
contar como límite — y sigue avisándose.
|
|
61
|
+
|
|
62
|
+
- **Una notificación de tarea de fondo ya no borra del chat a la persona que está mirando.** Cuando un
|
|
63
|
+
guard frena algo, el bloqueo ofrece dos salidas: contestar «dale» —corto, y es lo que la persona ya está
|
|
64
|
+
haciendo— o pegar líneas a mano en `planning/.ops-approval`. La primera sólo se ofrecía si el registro
|
|
65
|
+
de la sesión decía que había alguien.
|
|
66
|
+
|
|
67
|
+
Una notificación de una tarea de fondo entra por el mismo hook que un mensaje, así que el registro
|
|
68
|
+
pasaba a decir que el último que habló no era una persona — y en el turno que esa notificación despierta
|
|
69
|
+
**la persona sigue ahí**. El bloqueo le ofrecía la salida cara y terminaba copiando y pegando un comando
|
|
70
|
+
que no hacía falta. Se disparaba solo en el patrón más común de una sesión larga: lanzar trabajo de
|
|
71
|
+
fondo y retomar cuando vuelve.
|
|
72
|
+
|
|
73
|
+
Que haya alguien a quien preguntarle pasa a ser de la sesión y no del mensaje. **No cambia ninguna
|
|
74
|
+
autorización**: preguntar no es conceder, y lo que decide si un «dale» viejo cubre algo nuevo quedó
|
|
75
|
+
intacto — un mensaje de hace tres turnos sigue sin autorizar lo que se frenó ahora. Y un recorrido de
|
|
76
|
+
Cauce sigue sin ofrecer el «dale», porque ahí no hay nadie leyendo.
|
|
77
|
+
|
|
78
|
+
- **Una tarea que `autobuild` parte ya suelta su reserva, así que la corrida sigue.** El recorrido reclama
|
|
79
|
+
la tarea antes de estimarla, y cuando Decompose la partía el reclamo quedaba puesto sobre un slug que ya
|
|
80
|
+
no estaba ni en la cola ni en lo hecho. Con eso el runner quedaba ocupado por una tarea que no existe: el
|
|
81
|
+
Claim de la primera subtarea se negaba —«este runner ya tiene …»— y la corrida entera terminaba en
|
|
82
|
+
`claim-stuck` **sin construir nada**, dejando además `check` en rojo con un reclamo huérfano que soltaba
|
|
83
|
+
una persona a mano.
|
|
84
|
+
|
|
85
|
+
No dependía de una carrera ni de un id de runner compartido, que es a lo que el mensaje mandaba a mirar:
|
|
86
|
+
pasaba siempre que Decompose partiera, que es una fase que el propio protocolo manda correr. Si venís de
|
|
87
|
+
una versión anterior y tenés un reclamo huérfano de esto, se suelta con
|
|
88
|
+
`node tools/ops.js release planning <tarea>`.
|
|
89
|
+
|
|
90
|
+
La reserva se suelta **después** de comprobar que el reemplazo en el BACKLOG ocurrió: si no ocurrió, la
|
|
91
|
+
tarea sigue viva y su reserva tiene que seguir puesta.
|
|
92
|
+
|
|
93
|
+
- **Un veredicto se lee de una sola forma, así que las dos cuentas de un registro coinciden.** El registro
|
|
94
|
+
de una evaluación transcribe la respuesta literal del cargo —es la evidencia de la corrida—, así que su
|
|
95
|
+
cuerpo puede traer cualquier cosa, incluida una línea con forma de veredicto. La cuenta de la última
|
|
96
|
+
corrida las tomaba todas y la cuenta compuesta sólo las que cuelgan de un `### <caso>`: dos números
|
|
97
|
+
distintos sobre el mismo archivo y ninguna forma de saber cuál valía.
|
|
98
|
+
|
|
99
|
+
Con un registro de dos casos cuya respuesta citaba un veredicto ajeno, la última corrida decía «2 de 3»
|
|
100
|
+
sobre una corrida que midió dos y pasó las dos. Ahora un veredicto es un caso con su línea —sin
|
|
101
|
+
encabezado no hay a qué atribuirlo— y las dos lecturas salen del mismo lugar.
|
|
102
|
+
|
|
103
|
+
- **Un `agents fork` que se corta a la mitad se retira en vez de quedarse puesto.** Si el copiado fallaba
|
|
104
|
+
—un archivo ilegible, disco lleno— quedaba medio cargo en `agents/`, y lo caro no es perder la copia: es
|
|
105
|
+
dejarla. El catálogo pasa a resolver el slug contra esa copia, así que el intento siguiente ya no dice
|
|
106
|
+
que se cortó sino **«ya lo mantiene esta empresa»** — y lo que la empresa mantiene es un contrato
|
|
107
|
+
incompleto que ningún manifiesto registra, o sea que el aviso de deriva tampoco lo mira nunca.
|
|
108
|
+
|
|
109
|
+
Verificado provocando el corte con un archivo ilegible: antes el segundo intento contestaba «ya lo
|
|
110
|
+
mantiene esta empresa» con `.cauce` vacío; ahora no queda nada en `agents/` y arreglando lo que lo
|
|
111
|
+
cortó el fork sale entero. Sólo se borra lo que esa llamada creó, y la ruta se comprueba contra la raíz
|
|
112
|
+
de la instancia antes de tocarla.
|
|
113
|
+
|
|
114
|
+
- **`automation uninstall` no borra nada si no puede leer la configuración de tu runner.** Borraba
|
|
115
|
+
primero y leía el `settings.json` al final, con un `JSON.parse` sin proteger. Con un archivo que alguien
|
|
116
|
+
dejó a medio fusionar, el comando moría con «Expected property name or '}' in JSON at position 13»
|
|
117
|
+
—que no nombra ni un archivo— **después** de haber borrado los workflows y los cargos: la instancia
|
|
118
|
+
quedaba a medio desinstalar, con la configuración registrando guards que ya no existen.
|
|
119
|
+
|
|
120
|
+
Verificado sobre un banco: el comando se para antes de tocar el disco, dice qué archivo no pudo leer, y
|
|
121
|
+
arreglando ese archivo vuelve a correr.
|
|
122
|
+
|
|
123
|
+
- **Un adaptador propio que no trae `components` ya no rompe el borrador.** El README declara el contrato
|
|
124
|
+
de un adaptador por sus tres funciones y no dice qué campos trae un item, así que un adaptador de la
|
|
125
|
+
empresa —que es lo que ese README invita a escribir— puede no traerlo. Con `serviceFrom: "component"`,
|
|
126
|
+
que es lo que trae el molde, eso era un `TypeError` sobre `undefined` sin nada que lo atribuyera.
|
|
127
|
+
|
|
128
|
+
Sin el campo la respuesta es la que el borrador ya sabía dar: `service: ""` y «Debe definirse un
|
|
129
|
+
servicio único para la promoción». Lo que se toleró es la ausencia y no el caso de uso: con un
|
|
130
|
+
componente sigue resolviendo el servicio y con dos sigue sin resolverlo.
|
|
131
|
+
|
|
132
|
+
- **`promotionEpic` se valida también cuando la promoción es una épica.** El campo se exigía `NNN` sólo
|
|
133
|
+
para una historia. Para una épica es opcional —sin él el número lo elige el motor—, y por eso nadie lo
|
|
134
|
+
miraba; pero cuando está se usa tal cual para armar el nombre del archivo. O sea que el único caso sin
|
|
135
|
+
validar era el único que escribe una ruta con lo que diga el campo.
|
|
136
|
+
|
|
137
|
+
`promotionEpic: 7` producía `epic-7-…`, que el roadmap no reconoce como épica: la promoción se anunciaba
|
|
138
|
+
bien y lo escrito quedaba invisible para la cola. Y con un `..` la épica salía de `roadmap/` entera.
|
|
139
|
+
Verificado sobre una instancia desechable: `integration check` daba exit 0 y `integration promote`
|
|
140
|
+
devolvía «✓ jira:DEMO-42 promovido como epic» dejando el archivo fuera del roadmap. Ahora `check` lo
|
|
141
|
+
rechaza con «promotionEpic debe ser NNN» y `promote`, que valida antes de escribir, no llega a tocar el
|
|
142
|
+
disco.
|
|
143
|
+
|
|
144
|
+
- **El aviso de commits sin entrada de DONE vuelve a decir lo que ve.** El filtro por fecha leía la
|
|
145
|
+
columna del `git log` en una posición fija, y `%h` no mide siempre lo mismo: git sube el largo del hash
|
|
146
|
+
abreviado solo cuando el repositorio crece. En cuanto pasa de siete, la posición fija lee un espacio en
|
|
147
|
+
vez de la fecha y **ningún commit pasa el filtro**, así que el aviso quedaba mudo sin decir por qué —la
|
|
148
|
+
peor forma de que una puerta falle, porque se lee igual que «no hay nada que avisar»—. Ahora se parte
|
|
149
|
+
por espacios y se lee el campo.
|
|
150
|
+
|
|
151
|
+
Verificado en este repositorio, donde el abreviado mide ocho: el aviso pasó de cero a «66 commit(s)
|
|
152
|
+
desde 2026-09-15 que ninguna entrada de DONE nombra». La prueba nueva lo fija en 7, 8 y 12.
|
|
153
|
+
|
|
154
|
+
- **El sello de un registro de evaluación se escribe en el frontmatter y no en el cuerpo.**
|
|
155
|
+
`markConsolidated` buscaba `status:` en el documento entero. Un registro nace sin `status` en su
|
|
156
|
+
frontmatter y su cuerpo transcribe la respuesta literal del cargo: si esa respuesta traía una línea
|
|
157
|
+
`status: <algo>` en la columna cero, el sello aterrizaba ahí y el frontmatter quedaba sin sellar.
|
|
158
|
+
|
|
159
|
+
Un registro sin sellar vuelve a entrar en la propuesta siguiente, o sea que el mismo hallazgo llega dos
|
|
160
|
+
veces — que es exactamente lo que sellar existe para evitar.
|
|
161
|
+
|
|
162
|
+
- **`ops -h` pide la ayuda, y un nombre heredado de `Object` ya no pasa por comando.** Dos huecos de la
|
|
163
|
+
puerta de entrada del CLI, los dos de la misma forma: leer algo que no era ni un comando ni una bandera.
|
|
164
|
+
|
|
165
|
+
`-h` se anunciaba en el uso y no existía: el parser sólo reconoce lo que empieza con `--`, así que caía
|
|
166
|
+
de argumento posicional y `ops check -h` contestaba «no existe el planning en …/-h» **saliendo con 0**.
|
|
167
|
+
Pedir ayuda y recibir un error sobre un directorio inventado es la peor forma de contestar, porque
|
|
168
|
+
parece que el comando corrió.
|
|
169
|
+
|
|
170
|
+
Y la tabla de comandos se consultaba con `FLAGS[comando]`, que resuelve contra `Object.prototype`:
|
|
171
|
+
`ops constructor` salía con 0 sin hacer nada y `ops toString --json` moría con
|
|
172
|
+
«FLAGS[command].includes is not a function». No es un nombre exótico — es lo que sale de pasarle al CLI
|
|
173
|
+
una variable que vino vacía o mal leída desde un script.
|
|
174
|
+
|
|
175
|
+
- **El tope de horas de una propuesta deja de desaparecer cuando la configuración no lo declara.** La
|
|
176
|
+
validación de propuestas lee `runner.maxTaskHours` de `ops.config.json` y tenía un default de cuatro
|
|
177
|
+
horas para cuando no se puede leer. Ese default sólo cubría el archivo ilegible: un `runner` sin el
|
|
178
|
+
campo no lanza nada, así que el tope quedaba en `undefined` y la comparación pasaba a ser falsa
|
|
179
|
+
siempre. O sea que el límite no se aflojaba: se iba entero, y en silencio.
|
|
180
|
+
|
|
181
|
+
Se nota poco a propósito de lo que es — lo que deja de pasar es un **rechazo**—, así que una propuesta
|
|
182
|
+
de cuarenta horas viajaba a Jira sin que nada la mirara. Verificado sobre una configuración con
|
|
183
|
+
`runner: {}`: antes no salía ni un error, ahora sale «supera 4h; debe dividirse o justificarlo».
|
|
184
|
+
|
|
185
|
+
Si tu `ops.config.json` declara `maxTaskHours`, nada cambia — `check` ya lo exigía mayor que cero.
|
|
186
|
+
|
|
187
|
+
- **`rm -r` sobre la raíz o el home se frena también entrecomillado.** La regla reconocía el destino sólo
|
|
188
|
+
desnudo: `rm -rf /` bloqueaba y `rm -rf "/"`, `rm -rf "$HOME"`, `rm -rf ${HOME}`, `rm -rf $HOME/` y
|
|
189
|
+
`rm -rf -- /` pasaban. La comilla es lo que más importa — citar una variable es la forma *correcta* de
|
|
190
|
+
escribirlo en bash, así que el hueco premiaba al que tiene el hábito bueno.
|
|
191
|
+
|
|
192
|
+
Los borrados con destino concreto siguen pasando, también entrecomillados: se comprobó con
|
|
193
|
+
`rm -rf "$HOME/proyecto/dist"` entre otros cinco.
|
|
194
|
+
|
|
195
|
+
- **Un commit escrito en dos líneas vuelve a pasar por los guards.** `isCommit` reconocía el commit
|
|
196
|
+
después de `;`, `&` o `|` y no después de un salto de línea ni dentro de un subshell — o sea que
|
|
197
|
+
`git add x` ⏎ `git commit -m x` en un solo Bash, que es como se escriben dos pasos, dejaba sin correr
|
|
198
|
+
`governance`, `dependencies` y `verify`, y también el bloqueo que impide stagear y commitear a la vez.
|
|
199
|
+
|
|
200
|
+
`bash -c "git commit …"` sigue sin reconocerse, a propósito: hacerlo exige desentrecomillar, y eso
|
|
201
|
+
choca con que el mensaje de un commit sea dato y no código.
|
|
202
|
+
|
|
203
|
+
- **Redirigir con `>|` ya no evade el guard que mira a dónde escribe un comando.** `>|` es el override de
|
|
204
|
+
`noclobber` y escribe igual que `>`, pero el guard no veía su destino: el comando se partía en segmentos
|
|
205
|
+
por `|` antes de buscar destinos, así que la redirección quedaba separada de su ruta. Verificado sobre
|
|
206
|
+
un banco: `echo x > <fuera>` y `echo x >> <fuera>` bloqueaban y `echo x >| <fuera>` pasaba.
|
|
207
|
+
|
|
208
|
+
Importa porque una de las rutas que se escriben por ese hueco es `planning/.ops-approval` — la
|
|
209
|
+
aprobación que después habilita el push a la rama viva—, que es lo que cerraron el 098 y el 119.
|
|
210
|
+
|
|
211
|
+
Las tuberías corrientes siguen sin producir destino: se comprobó con `ls | wc -l` y `a || b`.
|
|
212
|
+
|
|
213
|
+
## [0.94.0] - 2026-09-16
|
|
214
|
+
|
|
215
|
+
### Agregado
|
|
216
|
+
|
|
217
|
+
- **Cinco reglas nuevas, traídas de una empresa que las pagó.** Salieron de revisar las reglas propias de
|
|
218
|
+
una instancia real: no son ideas, cada una tiene adentro la corrida que costó.
|
|
219
|
+
|
|
220
|
+
- **R24 — una premisa sobre el propio código se abre antes de usarla.** R14 ya exigía registro para lo
|
|
221
|
+
que se afirma de una herramienta o una norma, y dejaba afuera tu propio repositorio, que es donde
|
|
222
|
+
nadie te va a discutir. Cuatro corridas perdidas en un día, todas frenadas en la puerta y ninguna por
|
|
223
|
+
el código: la aceptación pedía algo que el sistema no hace. Y el ancla `archivo:línea` se abre, no se
|
|
224
|
+
copia — una función se movió de la 555 a la 733 en la misma sesión.
|
|
225
|
+
- **R25 — el identificador de una unidad de trabajo no cambia mientras está viva.** Renombrar un slug a
|
|
226
|
+
mitad de camino rompe el cruce entre la cola y lo hecho **sin que nada falle**: cada lado se lee
|
|
227
|
+
coherente por separado. Una tarea partida en cuatro y cerrada con otros nombres costó 594k tokens de
|
|
228
|
+
la corrida siguiente para descubrir que ya estaba construida. Si el nombre tiene que cambiar, va
|
|
229
|
+
`slug-nuevo (antes: slug-viejo)` hasta cerrar.
|
|
230
|
+
- **R26 — una puerta acota su propio costo y no escribe en el árbol que juzga.** Dos revisores lanzando
|
|
231
|
+
la misma suite fueron cuatro corridas en cuatro minutos: el sistema operativo mató la sesión entera
|
|
232
|
+
con un pico de 24,2 GB. Y un formateador con `--fix` o un build que limpia su salida editan el trabajo
|
|
233
|
+
de quien está commiteando. Una puerta que estorba se saltea, y desde ahí no protege de nada.
|
|
234
|
+
- **R27 — una defensa se aplica por defecto y cada excepción se declara sola.** Con lista de lo que
|
|
235
|
+
protege, todo lo que se agregue después nace afuera y nada lo compara. Incluye el caso que más se
|
|
236
|
+
disfraza: «esta comprobación no corre en desarrollo» es una quita escrita como agregado, y garantiza
|
|
237
|
+
que el camino de producción sea el único que nunca se ejercitó.
|
|
238
|
+
- **R28 — un estado lo dice el contenido de un archivo, nunca su presencia.** Un centinela cuya única
|
|
239
|
+
información es existir obliga a que borrarlo sea parte de la resolución, y eso alguien lo olvida: la
|
|
240
|
+
corrida arranca, lee todo el estado y recién ahí muere. Pasó dos veces el mismo día, a 42k tokens por
|
|
241
|
+
vez. Es lo que el WIP de Cauce ya hace bien con `status: IDLE`.
|
|
242
|
+
|
|
243
|
+
- **R9 dice cuándo se puede quitar lo que está en uso.** Exigía probar la ausencia de lo quitado y nunca
|
|
244
|
+
decía cuándo se puede quitar. Ahora: lo que está en uso no se corta, se depreca, y la marca dice las
|
|
245
|
+
dos cosas que la vuelven una salida y no una etiqueta — qué lo reemplaza, y qué condición permite
|
|
246
|
+
borrarlo. Sin la primera, quien lo usa no sabe a dónde ir; sin la segunda, el deprecado es código
|
|
247
|
+
muerto con un cartel puesto y se queda para siempre.
|
|
248
|
+
|
|
249
|
+
Y lo que no llama nadie es otra cosa: se borra. Cortar de golpe rompe a un consumidor que nadie miró;
|
|
250
|
+
deprecar lo que nadie usa cuesta mantener dos caminos para nadie.
|
|
251
|
+
|
|
252
|
+
- **R10 dice a dónde va lo que se publica, no sólo quién lo autoriza.** La autorización decía si se
|
|
253
|
+
publica y nunca dónde. Ahora: lo que se publica va al repositorio en el que estás trabajando, y si ese
|
|
254
|
+
remoto es un fork, va al fork — con la rama cortada de la suya, porque una rama cortada del principal
|
|
255
|
+
es la antesala de mandarle el PR.
|
|
256
|
+
|
|
257
|
+
No se deduce del contexto: que la herramienta resuelva sola el repositorio de origen no es una
|
|
258
|
+
autorización, ni lo son que el cambio «obviamente tenga que llegar ahí» ni que un PR anterior haya ido
|
|
259
|
+
a parar allá. Saltar al principal se pide con todas las letras y para ese caso concreto.
|
|
260
|
+
|
|
261
|
+
Es de las pocas sin vuelta atrás: un PR mal apuntado es trabajo publicado en el repositorio de otro
|
|
262
|
+
equipo — lo vieron, les llegó la notificación, y cerrarlo no deshace nada de eso.
|
|
263
|
+
|
|
264
|
+
- **R8 dice que la prohibición de firmas de IA cubre todo lo que se publica**, no sólo el mensaje del
|
|
265
|
+
commit: el título y el cuerpo del pull request, y los comentarios que se dejen ahí. Y casi nunca es
|
|
266
|
+
algo que alguien tipea — lo agrega la herramienta sola, al final del texto que escribiste—, así que
|
|
267
|
+
cumplirla es revisar la salida antes de publicarla, no acordarse de no escribirla.
|
|
268
|
+
|
|
269
|
+
- **R17 dice qué cuenta como una condición, que es lo que volvía incontable su umbral.** La barra son
|
|
270
|
+
cinco condiciones de aceptación y nunca decía qué es una. Ahora: una condición es un resultado que se
|
|
271
|
+
puede mirar por separado, no una viñeta. Cinco viñetas que describen el mismo invariante desde cinco
|
|
272
|
+
ángulos son **una**, y contarlas como cinco parte por la mitad lo que era una sola cosa.
|
|
273
|
+
|
|
274
|
+
La otra dirección es la cara y la que nadie mira: una frase que promete dos resultados con vidas
|
|
275
|
+
distintas —«valida el pago y manda el email»— son **dos**, y escrita como una el umbral no se entera
|
|
276
|
+
nunca. Contar de menos no dispara nada, y se lee igual que una unidad chica.
|
|
277
|
+
|
|
278
|
+
La prueba no pide criterio: si al tachar una condición las otras siguen valiendo, son distintas; si
|
|
279
|
+
tachar una deja a las demás sin sentido, era una sola dicha en partes.
|
|
280
|
+
|
|
281
|
+
- **R9 ahora pide que la mutación quede escrita, no sólo que se corra.** R9 ya exigía romper, con el
|
|
282
|
+
código puesto, exactamente lo que el caso dice cuidar, y verlo ponerse rojo. Lo que faltaba es que eso
|
|
283
|
+
quedara en la aceptación: una línea con qué se rompe y qué prueba tiene que ponerse roja. Sin ella,
|
|
284
|
+
quien revisa no puede distinguir la mutación que se corrió de la que se pensó, y lo único que le queda
|
|
285
|
+
es volver a correrla — o sea rehacer el trabajo que delegarlo evitaba.
|
|
286
|
+
|
|
287
|
+
Y escribirla antes cambia lo que se escribe: una aceptación que tiene que nombrar qué romper deja de
|
|
288
|
+
poder pedir algo que ninguna mutación puede tocar. Si no hay nada que romper, no había propiedad que
|
|
289
|
+
cuidar, y eso se ve al redactarla en vez de al final de la vuelta.
|
|
290
|
+
|
|
291
|
+
**Lo que cuesta:** el bloque de reglas que cada agente carga al arrancar pasa de **39,1 a 49,1 KB**.
|
|
292
|
+
Está medido, no estimado, y el umbral del aviso de `check` **no se movió**: sigue en 64 KB, porque lo
|
|
293
|
+
que mide es cuánto agregaste vos, y subirlo para hacerle lugar al piso apagaría justamente eso. Quedan
|
|
294
|
+
~18 KB de margen antes de que el aviso hable.
|
|
295
|
+
|
|
296
|
+
### Corregido
|
|
297
|
+
|
|
298
|
+
- **El aviso de peso mide lo que agregaste vos, no el total.** Comparaba el bloque entero contra el
|
|
299
|
+
umbral, así que el piso del toolkit y tus reglas salían del mismo bolsillo: decía «tu bloque pesa»
|
|
300
|
+
cuando la mitad la habíamos puesto nosotros, y cada regla que Cauce agregaba te achicaba el margen sin
|
|
301
|
+
que nadie lo decidiera. Ahora el umbral se compara contra tus reglas, y la línea dice las dos cosas —
|
|
302
|
+
`114.8 KB en cada agente (93.8 KB propias)`—: el total es lo que paga el agente y lo propio es lo único
|
|
303
|
+
sobre lo que podés hacer algo.
|
|
304
|
+
|
|
305
|
+
**Una instancia recién creada ya no puede cruzarlo**, por más que el piso crezca. Antes era una cuenta
|
|
306
|
+
que había que rehacer cada vez que agregábamos una regla.
|
|
307
|
+
|
|
308
|
+
El número **no se movió**: sigue en 64 KB, a propósito, porque cambiar qué se mide y cuánto a la vez
|
|
309
|
+
deja sin saber cuál de los dos movió el resultado. Lo que sí quedó medido es que 64 está por debajo de
|
|
310
|
+
lo que una empresa real usa — una instancia medida tiene 93,8 KB de reglas propias — así que elegirlo
|
|
311
|
+
con esa evidencia es lo que sigue.
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
- **Escribir tu propio `process.md` ya no te deja sin las reglas que no ibas a reemplazar.** El override
|
|
315
|
+
es por nombre de archivo, así que reemplazar «pensar antes de editar» por tu versión se llevaba puesto
|
|
316
|
+
el archivo entero: R16, R17, R20, R21 y R22 dejaban de llegarle a todo agente. `check` te lo decía —lo
|
|
317
|
+
hace desde 0.57.0— y no había nada que hacer al respecto, porque conservarlas exigía copiar su texto y
|
|
318
|
+
una copia deja de recibir las mejoras del `upgrade`.
|
|
319
|
+
|
|
320
|
+
Ahora **R16, R20, R21 y R22 viven en `system/runs.md`** —lo que cuesta una corrida, cuándo una medición
|
|
321
|
+
vale, cómo se retoma lo interrumpido y qué no se toca mientras se mide—, un archivo que reemplazar tu
|
|
322
|
+
proceso no toca. `system/process.md` se queda con R1..R4 y R17.
|
|
323
|
+
|
|
324
|
+
**No tenés que hacer nada**: el archivo nuevo llega en tu próximo `upgrade`. Si ya sobrescribiste
|
|
325
|
+
`process.md`, esas cuatro reglas vuelven a regir solas, y el aviso de `check` se acorta a lo que de
|
|
326
|
+
verdad reemplazaste. El bloque de reglas pasa de cuatro archivos a cinco y pesa lo mismo: 39,1 KB.
|
|
327
|
+
|
|
17
328
|
## [0.93.0] - 2026-09-16
|
|
18
329
|
|
|
19
330
|
### Corregido
|
|
@@ -650,8 +650,23 @@ while (rounds++ < MAX_TASKS) {
|
|
|
650
650
|
)
|
|
651
651
|
if (!estimate) return stop('agent-unavailable', 'Decompose no devolvió resultado')
|
|
652
652
|
if (estimate.needsSplit) {
|
|
653
|
+
// Una tarea que viene de una épica es además una historia suya, y la lista de historias no se
|
|
654
|
+
// actualizaba sola: quedaba nombrando un slug que ya no existe en ninguna parte. Medido sobre un
|
|
655
|
+
// banco, eso rompe por los dos lados según qué escriba el agente — con las subtareas declarando
|
|
656
|
+
// la épica, `check` se pone en rojo en el acto («BACKLOG <sub>: no existe en epic-NNN»); sin
|
|
657
|
+
// declararla pasa en verde y la épica **no puede cerrar nunca**, porque `closed` exige evidencia
|
|
658
|
+
// de cada historia y la original jamás la va a tener.
|
|
659
|
+
//
|
|
660
|
+
// Es lo que R25 pide al decir que la unidad partida se cierra diciendo en qué se partió, y el
|
|
661
|
+
// lugar donde eso se dice es la épica: ahí es donde la unidad vivía. En `done/` no va —su README
|
|
662
|
+
// declara que es la evidencia de lo que una tarea **entregó**, y una partida no entregó nada—
|
|
663
|
+
// (caso 169).
|
|
664
|
+
const historias = task.epic
|
|
665
|
+
? ` ${task.id} es una historia de la épica ${task.epic}: reemplazá también ahí su historia por `
|
|
666
|
+
+ 'las de las subtareas, con el mismo criterio y el mismo service que traía.'
|
|
667
|
+
: ''
|
|
653
668
|
await write(`Reemplazá sólo ${task.id} en ${BACKLOG} por subtareas ordenadas y verificables de forma ` +
|
|
654
|
-
`independiente: ${JSON.stringify(estimate.subtasks)}
|
|
669
|
+
`independiente: ${JSON.stringify(estimate.subtasks)}.${historias}`, { label: 'split' })
|
|
655
670
|
planning = await readContext()
|
|
656
671
|
if (!planning) return stop('context-unavailable', `no se pudo releer el estado de ${P}`)
|
|
657
672
|
// Misma forma que en Claim: si la cola sigue ofreciendo lo mismo, el estado no cambió y repetir
|
|
@@ -661,6 +676,18 @@ while (rounds++ < MAX_TASKS) {
|
|
|
661
676
|
return stop('split-not-applied', `se pidió reemplazar ${task.id} en ${BACKLOG} por sus `
|
|
662
677
|
+ 'subtareas y la cola sigue ofreciéndola: la escritura no ocurrió como se pidió.')
|
|
663
678
|
}
|
|
679
|
+
// La tarea partida ya no existe, así que su reclamo no reserva nada: lo único que hace es dejar
|
|
680
|
+
// al runner ocupado por un slug que no está ni en la cola ni en lo hecho. Ahí el Claim de la
|
|
681
|
+
// primera subtarea se niega —«este runner ya tiene …»— y la corrida entera para con
|
|
682
|
+
// `claim-stuck` sin construir nada. El reclamo lo puso esta corrida; soltarlo también le toca.
|
|
683
|
+
//
|
|
684
|
+
// Va **después** de la guarda de arriba y no antes de leer el contexto: si el reemplazo no
|
|
685
|
+
// ocurrió, la tarea sigue viva y su reclamo tiene que seguir puesto (caso 163).
|
|
686
|
+
await write(
|
|
687
|
+
`Corré "node tools/ops.js release ${P} ${task.id}" desde ${ROOT}: quedó partida y su reserva `
|
|
688
|
+
+ 'ya no aplica. No escribas ningún archivo vos: lo escribe el comando.',
|
|
689
|
+
{ label: `release:${task.id}` },
|
|
690
|
+
)
|
|
664
691
|
log(`${task.id} quedó partida: la corrida sigue con ${nextUp(planning)}`)
|
|
665
692
|
continue
|
|
666
693
|
}
|
|
@@ -186,6 +186,22 @@ function nextResult(root, agent, date, kind) {
|
|
|
186
186
|
return `${date}-${run}.md`
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Qué veredictos trae un registro, leídos en un solo lugar porque `latest` y `composed` contestan sobre
|
|
190
|
+
// el mismo archivo y contestaban distinto: `latest` contaba cualquier línea `- Veredicto:` y `composed`
|
|
191
|
+
// sólo la que cuelga de un `### <caso>`. La diferencia no es teórica — el registro transcribe la
|
|
192
|
+
// respuesta literal del cargo, así que una respuesta que cita un veredicto ajeno sumaba un caso que no
|
|
193
|
+
// existe, y `latest` decía «2 de 3» sobre una corrida que midió dos y pasó las dos.
|
|
194
|
+
//
|
|
195
|
+
// Un veredicto es un caso con su línea, nunca una línea suelta: sin el encabezado no hay a qué atribuirlo.
|
|
196
|
+
const VERDICT = /\n###\s+([^\n]+)\n\n-\s*Veredicto:\s*(pasa|no pasa)\s*$/gim
|
|
197
|
+
|
|
198
|
+
function caseVerdicts(text) {
|
|
199
|
+
return [...text.matchAll(VERDICT)].map((hit) => ({
|
|
200
|
+
id: hit[1].trim(),
|
|
201
|
+
passed: hit[2].toLowerCase() === 'pasa',
|
|
202
|
+
}))
|
|
203
|
+
}
|
|
204
|
+
|
|
189
205
|
// El último resultado registrado, para que `evaluate` pueda decir si el cargo se corrió alguna vez y
|
|
190
206
|
// cómo le fue. No es un error no tenerlo: correrlo cuesta, y exigirlo en CI sería exigir red.
|
|
191
207
|
function latest(root, agent, kind) {
|
|
@@ -195,7 +211,7 @@ function latest(root, agent, kind) {
|
|
|
195
211
|
const name = names[names.length - 1]
|
|
196
212
|
const file = path.join(dir, name)
|
|
197
213
|
const text = fs.readFileSync(file, 'utf8')
|
|
198
|
-
const verdicts =
|
|
214
|
+
const verdicts = caseVerdicts(text)
|
|
199
215
|
const [, date, run] = name.match(RESULT_NAME)
|
|
200
216
|
return {
|
|
201
217
|
file,
|
|
@@ -204,7 +220,7 @@ function latest(root, agent, kind) {
|
|
|
204
220
|
date,
|
|
205
221
|
run: Number(run || 1),
|
|
206
222
|
total: verdicts.length,
|
|
207
|
-
passed: verdicts.filter((verdict) => verdict
|
|
223
|
+
passed: verdicts.filter((verdict) => verdict.passed).length,
|
|
208
224
|
}
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -225,9 +241,8 @@ function composed(root, agent, kind) {
|
|
|
225
241
|
for (const name of names) {
|
|
226
242
|
const file = path.join(dir, name)
|
|
227
243
|
const [, date] = name.match(RESULT_NAME)
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
current.set(hit[1].trim(), { passed: hit[2].toLowerCase() === 'pasa', date, file })
|
|
244
|
+
for (const one of caseVerdicts(fs.readFileSync(file, 'utf8'))) {
|
|
245
|
+
current.set(one.id, { passed: one.passed, date, file })
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
248
|
const entries = [...current.entries()]
|
package/engine/agents/fork.js
CHANGED
|
@@ -16,7 +16,7 @@ const path = require('node:path')
|
|
|
16
16
|
const catalog = require('./catalog')
|
|
17
17
|
const manifest = require('../core/manifest')
|
|
18
18
|
const ownership = require('../core/ownership')
|
|
19
|
-
const { atomicWrite } = require('../core/files')
|
|
19
|
+
const { atomicWrite, assertWithin } = require('../core/files')
|
|
20
20
|
|
|
21
21
|
// Informes, propuestas y veredictos no viajan: son lo que produjo nuestra versión del contrato, y el
|
|
22
22
|
// fork nace para dejar de ser ese contrato. Heredar un veredicto le daría a la copia una garantía que
|
|
@@ -80,13 +80,26 @@ function fork(root, slug, date) {
|
|
|
80
80
|
const fromPath = `agents/${type}/system/${slug}`
|
|
81
81
|
const toPath = `agents/${type}/${slug}`
|
|
82
82
|
const digests = {}
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
try {
|
|
84
|
+
for (const relative of files) {
|
|
85
|
+
const from = path.join(found.dir, relative)
|
|
86
|
+
const to = path.join(target, relative)
|
|
87
|
+
fs.mkdirSync(path.dirname(to), { recursive: true })
|
|
88
|
+
if (TEXT.test(relative)) fs.writeFileSync(to, fs.readFileSync(from, 'utf8').split(fromPath).join(toPath))
|
|
89
|
+
else fs.copyFileSync(from, to)
|
|
90
|
+
digests[relative] = manifest.digest(from)
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Lo caro de un copiado cortado no es perder la copia: es dejarla. Con medio cargo puesto el
|
|
94
|
+
// catálogo pasa a resolver el slug contra esa copia, así que el intento siguiente no dice «se
|
|
95
|
+
// cortó» sino «ya lo mantiene esta empresa» —y lo que la empresa mantiene es un contrato
|
|
96
|
+
// incompleto que ningún manifiesto registra, o sea que `drift` tampoco lo mira—.
|
|
97
|
+
//
|
|
98
|
+
// Se borra sólo lo que esta llamada creó: `target` se comprobó inexistente antes de empezar, y
|
|
99
|
+
// `assertWithin` lo nombra contra la raíz de la instancia antes de tocar nada —el slug entra por
|
|
100
|
+
// la línea de comandos y ésta es la única parte de este archivo que destruye—.
|
|
101
|
+
fs.rmSync(assertWithin(root, target, `el fork de ${slug}`), { recursive: true, force: true })
|
|
102
|
+
throw error
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
const version = packageVersion(root)
|
|
@@ -175,12 +175,12 @@ function pendingSources(text) {
|
|
|
175
175
|
if (one) out.push(one)
|
|
176
176
|
one = {}
|
|
177
177
|
last = ''
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
178
|
+
const first = line.match(/^\s{2}-\s*(name|url|why|since):\s*(.+?)\s*$/)
|
|
179
|
+
if (first) { one[first[1]] = quitar(first[2]); last = first[1] }
|
|
180
180
|
continue
|
|
181
181
|
}
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
182
|
+
const field = line.match(PENDING_FIELD)
|
|
183
|
+
if (field && one) { one[field[1]] = quitar(field[2]); last = field[1]; continue }
|
|
184
184
|
const sigue = line.match(/^\s{6,}(\S.*?)\s*$/)
|
|
185
185
|
if (sigue && one && last) one[last] += ` ${sigue[1]}`
|
|
186
186
|
}
|
|
@@ -229,9 +229,9 @@ function evaluate(root, agent) {
|
|
|
229
229
|
// Una pendiente sin razón es un enlace muerto con fecha, y sin fecha no se puede ver que lleva
|
|
230
230
|
// meses ahí. Los dos campos son la mitad del valor de la lista: lo que la vuelve revisable.
|
|
231
231
|
for (const one of pendingSources(fs.readFileSync(sourcesFile, 'utf8'))) {
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
234
|
-
errors.push(`sources.yaml: una pendiente no declara ${
|
|
232
|
+
const missing = ['name', 'why', 'since'].filter((key) => !one[key])
|
|
233
|
+
if (missing.length) {
|
|
234
|
+
errors.push(`sources.yaml: una pendiente no declara ${missing.join(' ni ')}`
|
|
235
235
|
+ `${one.name ? ` (${one.name})` : ''}`)
|
|
236
236
|
}
|
|
237
237
|
// Declarada y pendiente a la vez es una contradicción que el chequeo semanal no puede resolver:
|
|
@@ -18,16 +18,25 @@ const { SOURCE_TIERS, cadence, evaluate, evaluateTeam } = require('./learning-so
|
|
|
18
18
|
// consolidó y el que se escribió tarde —después de que la propuesta del período ya existía, y por eso
|
|
19
19
|
// no entra a ninguna— se leían igual. El segundo no es un descuido de forma: es un hallazgo que no
|
|
20
20
|
// llega al contrato y que nada delata. Marcar al primero es lo que deja ver al segundo.
|
|
21
|
+
// El sello se busca **dentro del frontmatter** y no en el documento entero. Un registro de corrida nace
|
|
22
|
+
// sin `status` ahí y su cuerpo lleva la respuesta verbatim del cargo: si esa respuesta traía una línea
|
|
23
|
+
// `status: <palabra>` a columna cero, el sello caía en el cuerpo y el frontmatter quedaba sin marcar —o
|
|
24
|
+
// sea el registro sin sellar, y el mismo hallazgo entrando a la propuesta siguiente, que es exactamente
|
|
25
|
+
// lo que sellar existe para evitar (caso 168).
|
|
26
|
+
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/
|
|
27
|
+
const STATUS_LINE = /^status:\s*\S+\s*$/m
|
|
28
|
+
|
|
21
29
|
function markConsolidated(file) {
|
|
22
30
|
const text = fs.readFileSync(file, 'utf8')
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
const front = text.match(FRONTMATTER)
|
|
32
|
+
if (front && STATUS_LINE.test(front[1])) {
|
|
33
|
+
return atomicWrite(file, text.replace(front[0],
|
|
34
|
+
`---\n${front[1].replace(STATUS_LINE, 'status: consolidated')}\n---\n`))
|
|
25
35
|
}
|
|
26
36
|
// Un informe nace con `status`; un registro de corrida no, porque lo escribe el recorrido de
|
|
27
37
|
// evaluación y ahí el dato no existía. Se agrega en vez de exigirle a quien lo escriba que se
|
|
28
38
|
// acuerde: el sello es lo que evita que el mismo hallazgo entre dos veces, y depender de una
|
|
29
39
|
// convención para eso es depender de que nadie la olvide.
|
|
30
|
-
const front = text.match(/^---\n([\s\S]*?)\n---\n/)
|
|
31
40
|
if (!front) return
|
|
32
41
|
atomicWrite(file, text.replace(front[0], `---\n${front[1]}\nstatus: consolidated\n---\n`))
|
|
33
42
|
}
|
|
@@ -206,9 +215,9 @@ function verdictFindings(root, dir) {
|
|
|
206
215
|
// encabezado, así que uno que escape puede hacer que se lea la sección equivocada.
|
|
207
216
|
const nested = (detail) => detail.replace(/^(#{1,5}) /gm, '#$1 ')
|
|
208
217
|
const findings = [...latest.values()].flatMap((item) => {
|
|
209
|
-
const
|
|
218
|
+
const runLine = `Corrida: \`${path.relative(root, item.file)}\``
|
|
210
219
|
if (!item.passed) {
|
|
211
|
-
return [`### ${item.id} — ${item.name.slice(0, -3)}\n\n${
|
|
220
|
+
return [`### ${item.id} — ${item.name.slice(0, -3)}\n\n${runLine}`
|
|
212
221
|
+ `${item.failures > 1 ? ` — falló en ${item.failures} corridas de esta tanda` : ''}\n\n`
|
|
213
222
|
+ `${nested(item.detail)}`]
|
|
214
223
|
}
|
|
@@ -222,7 +231,7 @@ function verdictFindings(root, dir) {
|
|
|
222
231
|
// y ahí una línea con este prefijo sería una nota que el sujeto se escribe a sí mismo.
|
|
223
232
|
const note = (item.detail.split('\n', 1)[0].match(CONTRACT_NOTE) || [])[1]
|
|
224
233
|
return note
|
|
225
|
-
? [`### ${item.id} — ${item.name.slice(0, -3)} · el caso pasa\n\n${
|
|
234
|
+
? [`### ${item.id} — ${item.name.slice(0, -3)} · el caso pasa\n\n${runLine}\n\n`
|
|
226
235
|
+ `Lo que el contrato no cubre: ${note.trim()}`]
|
|
227
236
|
: []
|
|
228
237
|
})
|
|
@@ -52,9 +52,9 @@ function check(root) {
|
|
|
52
52
|
}
|
|
53
53
|
// Un choque que `upgrade` conservó (caso 110) también queda distinto del paquete, y mandarlo a correr
|
|
54
54
|
// `upgrade` era una vuelta sin salida: lo conservaría otra vez. Se dice qué es y qué hacer.
|
|
55
|
-
const
|
|
55
|
+
const collisions = new Set(O.collisions(root))
|
|
56
56
|
for (const { file, edited } of staleHooks(root)) {
|
|
57
|
-
if (
|
|
57
|
+
if (collisions.has(`automatization/hooks/${file}`)) {
|
|
58
58
|
errors.push(`automatization/hooks/${file}: es tuyo y se llama como uno que trae el paquete, así que el `
|
|
59
59
|
+ "del paquete no está instalado; renombrá el tuyo y corré `cauce upgrade`")
|
|
60
60
|
continue
|
|
@@ -173,16 +173,16 @@ function isSharedFile(root, target) {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
function withoutBlock(text, name) {
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
const until = text.indexOf(blockEnd(name),
|
|
176
|
+
const from = text.indexOf(blockStart(name))
|
|
177
|
+
if (from === -1) return text
|
|
178
|
+
const until = text.indexOf(blockEnd(name), from)
|
|
179
179
|
if (until === -1) return text
|
|
180
|
-
return `${text.slice(0,
|
|
180
|
+
return `${text.slice(0, from)}${text.slice(until + blockEnd(name).length)}`.trimEnd()
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
function mergeInstruction(file, name, content) {
|
|
184
|
-
const
|
|
185
|
-
const instructionBody = withoutBlock(
|
|
184
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
185
|
+
const instructionBody = withoutBlock(current, name).trimEnd()
|
|
186
186
|
const block = `${blockStart(name)}\n\n${content.trim()}\n\n${blockEnd(name)}\n`
|
|
187
187
|
F.atomicWrite(file, instructionBody ? `${instructionBody}\n\n${block}` : block)
|
|
188
188
|
}
|
|
@@ -190,10 +190,10 @@ function mergeInstruction(file, name, content) {
|
|
|
190
190
|
function blockUpToDate(file, name, content) {
|
|
191
191
|
if (!fs.existsSync(file)) return false
|
|
192
192
|
const body = fs.readFileSync(file, 'utf8')
|
|
193
|
-
const
|
|
193
|
+
const from = body.indexOf(blockStart(name))
|
|
194
194
|
const until = body.indexOf(blockEnd(name))
|
|
195
|
-
if (
|
|
196
|
-
return body.slice(
|
|
195
|
+
if (from === -1 || until === -1) return false
|
|
196
|
+
return body.slice(from + blockStart(name).length, until).trim() === content.trim()
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
module.exports = {
|