@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/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,244 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.96.0] - 2026-09-16
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **Tres errores que el CLI contestaba con exit 1 ahora contestan 2, que es lo que dice la convención.**
|
|
22
|
+
Son «no se llegó a la pregunta»: `integrations/config.json` ilegible, un proveedor que no está en ese
|
|
23
|
+
registro, y un `package.json` inválido al declarar el motor. Sus hermanos —un `ops.config.json`
|
|
24
|
+
ilegible, un proveedor que Cauce no trae— ya contestaban 2.
|
|
25
|
+
|
|
26
|
+
El corte, que hasta ahora estaba en la cabeza de quien escribía cada salida, está escrito y sale en
|
|
27
|
+
`ops --help`: **2** es que el comando no existe, falta un argumento, o la raíz que nombrás no es lo que
|
|
28
|
+
dice ser —lo arreglás cambiando la invocación—; **1** es que se llegó y la respuesta es que no —una
|
|
29
|
+
validación encontró problemas, el estado se niega, o una operación falló a mitad de camino—.
|
|
30
|
+
|
|
31
|
+
Si tenés un script que distingue los dos, revisá esos tres casos. Si sólo mira «distinto de cero», no
|
|
32
|
+
cambia nada.
|
|
33
|
+
|
|
34
|
+
- **`ops flow list|check|show` acepta la raíz de la instancia, como el resto.** Todos los comandos que leen
|
|
35
|
+
una instancia la toman como posicional; `flow` la descartaba y resolvía por el directorio actual, así que
|
|
36
|
+
`ops flow list <raíz>` contestaba sobre otra cosa — y una lista de recorridos se lee igual de bien venga
|
|
37
|
+
de donde venga. Ahora es `ops flow list [ops-root]`, y con recorrido va después de él.
|
|
38
|
+
|
|
39
|
+
- **Una lista de cargos o recorridos vacía dice si es porque no hay o porque no se pudo resolver el
|
|
40
|
+
paquete.** Eran dos hechos distintos con la misma respuesta, y las acciones son opuestas. El aviso sale
|
|
41
|
+
por `stderr` y sólo cuando la lista viene vacía; el código de salida y el `--json` no cambian, porque
|
|
42
|
+
ese JSON lo consume el cron del ciclo de aprendizaje.
|
|
43
|
+
|
|
44
|
+
- **`automation check` nombra un `ops.config.json` ilegible en vez de culpar al motor.** La resolución del
|
|
45
|
+
paquete cuelga de esa configuración, así que un JSON roto se propagaba como archivos del motor que faltan
|
|
46
|
+
y el aviso mandaba a correr `npm install` sobre un motor instalado: la acción sugerida no arreglaba nada.
|
|
47
|
+
|
|
48
|
+
- **El catálogo de cargos y recorridos se encuentra también cuando el paquete vive un nivel arriba.** Es el
|
|
49
|
+
layout que documenta el arranque —`npm install @ingeniomaps/cauce` en la carpeta de la empresa y la
|
|
50
|
+
instancia adentro—, y 0.92.0 lo arregló para el motor y dejó afuera el catálogo. En ese estado `check`
|
|
51
|
+
pasaba en verde y `agents list` contestaba **una lista vacía con exit 0**, indistinguible de «esta
|
|
52
|
+
instancia no tiene cargos»; `flow list` igual, y `evaluate` culpaba a un `SKILL.md` que falta, mandando
|
|
53
|
+
a mirar el cargo en vez del paquete.
|
|
54
|
+
|
|
55
|
+
Si te pasó, no hacía falta bajar una segunda copia: con esta versión los cargos aparecen donde ya estaban.
|
|
56
|
+
Verificado instalando el paquete publicado desde el registro y recorriendo la superficie del CLI: de 0
|
|
57
|
+
cargos y 0 recorridos a **53 y 7**, sobre la misma instancia.
|
|
58
|
+
|
|
59
|
+
## [0.95.0] - 2026-09-16
|
|
60
|
+
|
|
61
|
+
### Corregido
|
|
62
|
+
|
|
63
|
+
- **El bloqueo dice hasta dónde llega el «dale».** Ofrece dos salidas y sólo la del pegado decía su
|
|
64
|
+
alcance —«valen para ese conjunto y dejan de valer en cuanto cambie»—; la del chat, que es la que se
|
|
65
|
+
ofrece primero, decía sólo «reintentá el mismo cambio y pasa».
|
|
66
|
+
|
|
67
|
+
Faltaban **dos** cosas y no son la misma. Que el «dale» cubre lo que se frenó y nada más: por eso en una
|
|
68
|
+
carpeta donde se itera cada archivo nuevo vuelve a frenar, que desde afuera se lee como si el guard se
|
|
69
|
+
hubiera olvidado de que ya la aprobaste. Y que lo concedido **sigue valiendo en los mensajes
|
|
70
|
+
siguientes** hasta que lo niegues — la mitad permisiva, la que no se nota porque lo que no ocurre es un
|
|
71
|
+
bloqueo. `check` la muestra al final de la corrida; ahora también se dice al concederla.
|
|
72
|
+
|
|
73
|
+
No cambia ningún permiso: el diff es texto.
|
|
74
|
+
|
|
75
|
+
- **Partir una tarea de una épica actualiza también sus historias, así que `check` no queda en rojo.** Una
|
|
76
|
+
tarea que viene de una épica es además una historia suya, y la partición reemplazaba sólo su línea del
|
|
77
|
+
BACKLOG. La lista de historias quedaba nombrando un slug que ya no existe en ninguna parte, y eso rompe
|
|
78
|
+
por los dos lados según qué escriba la partición: con las subtareas declarando la épica, `check` falla en
|
|
79
|
+
el acto con «BACKLOG \<sub\>: no existe en epic-NNN»; sin declararla pasa en verde y **la épica no puede
|
|
80
|
+
cerrar nunca**, porque `closed` exige evidencia de cada historia y la original jamás la va a tener.
|
|
81
|
+
|
|
82
|
+
Ahí es donde la unidad partida se cierra diciendo en qué se partió, que es lo que pide R25. **No va una
|
|
83
|
+
entrada en `done/`**: ese directorio es la evidencia de lo que una tarea entregó, y una partida no
|
|
84
|
+
entregó nada. Una tarea sin épica no necesita ningún cierre — deja de existir limpiamente y no hay
|
|
85
|
+
cruce que se rompa.
|
|
86
|
+
|
|
87
|
+
Si tenés una épica con una historia que ninguna tarea de la cola nombra y que nunca vas a poder cerrar,
|
|
88
|
+
viene de esto: reemplazá esa historia por las de las subtareas que la partición dejó en el BACKLOG.
|
|
89
|
+
|
|
90
|
+
- **Un límite escrito en dos líneas ya viaja entero a los agentes.** Una viñeta bajo `### Límites` se
|
|
91
|
+
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
|
|
92
|
+
escribe cualquiera de verdad— llegaba **sólo la primera línea**, cortada a mitad de frase. Y lo que se
|
|
93
|
+
pierde ahí suele ser lo que el límite decide: «…crear, editar o borrar productos, pedidos o» sin el «no
|
|
94
|
+
las hace un runner» que venía abajo.
|
|
95
|
+
|
|
96
|
+
La continuación, además, volvía por el otro lado: no coincidía con ninguna viñeta declarada y `check` la
|
|
97
|
+
reportaba como un párrafo «que no llega a los agentes», citando media frase tuya. O sea que el proyecto
|
|
98
|
+
hacía lo que el aviso pedía, el aviso seguía sonando, y el límite que sí viajaba era el mutilado.
|
|
99
|
+
|
|
100
|
+
Si escribiste tus límites en una sola línea para que el aviso bajara a cero, ya podés volver a partirlos
|
|
101
|
+
donde corresponda. Una línea en blanco cierra la viñeta, así que la prosa que venga después sigue sin
|
|
102
|
+
contar como límite — y sigue avisándose.
|
|
103
|
+
|
|
104
|
+
- **Una notificación de tarea de fondo ya no borra del chat a la persona que está mirando.** Cuando un
|
|
105
|
+
guard frena algo, el bloqueo ofrece dos salidas: contestar «dale» —corto, y es lo que la persona ya está
|
|
106
|
+
haciendo— o pegar líneas a mano en `planning/.ops-approval`. La primera sólo se ofrecía si el registro
|
|
107
|
+
de la sesión decía que había alguien.
|
|
108
|
+
|
|
109
|
+
Una notificación de una tarea de fondo entra por el mismo hook que un mensaje, así que el registro
|
|
110
|
+
pasaba a decir que el último que habló no era una persona — y en el turno que esa notificación despierta
|
|
111
|
+
**la persona sigue ahí**. El bloqueo le ofrecía la salida cara y terminaba copiando y pegando un comando
|
|
112
|
+
que no hacía falta. Se disparaba solo en el patrón más común de una sesión larga: lanzar trabajo de
|
|
113
|
+
fondo y retomar cuando vuelve.
|
|
114
|
+
|
|
115
|
+
Que haya alguien a quien preguntarle pasa a ser de la sesión y no del mensaje. **No cambia ninguna
|
|
116
|
+
autorización**: preguntar no es conceder, y lo que decide si un «dale» viejo cubre algo nuevo quedó
|
|
117
|
+
intacto — un mensaje de hace tres turnos sigue sin autorizar lo que se frenó ahora. Y un recorrido de
|
|
118
|
+
Cauce sigue sin ofrecer el «dale», porque ahí no hay nadie leyendo.
|
|
119
|
+
|
|
120
|
+
- **Una tarea que `autobuild` parte ya suelta su reserva, así que la corrida sigue.** El recorrido reclama
|
|
121
|
+
la tarea antes de estimarla, y cuando Decompose la partía el reclamo quedaba puesto sobre un slug que ya
|
|
122
|
+
no estaba ni en la cola ni en lo hecho. Con eso el runner quedaba ocupado por una tarea que no existe: el
|
|
123
|
+
Claim de la primera subtarea se negaba —«este runner ya tiene …»— y la corrida entera terminaba en
|
|
124
|
+
`claim-stuck` **sin construir nada**, dejando además `check` en rojo con un reclamo huérfano que soltaba
|
|
125
|
+
una persona a mano.
|
|
126
|
+
|
|
127
|
+
No dependía de una carrera ni de un id de runner compartido, que es a lo que el mensaje mandaba a mirar:
|
|
128
|
+
pasaba siempre que Decompose partiera, que es una fase que el propio protocolo manda correr. Si venís de
|
|
129
|
+
una versión anterior y tenés un reclamo huérfano de esto, se suelta con
|
|
130
|
+
`node tools/ops.js release planning <tarea>`.
|
|
131
|
+
|
|
132
|
+
La reserva se suelta **después** de comprobar que el reemplazo en el BACKLOG ocurrió: si no ocurrió, la
|
|
133
|
+
tarea sigue viva y su reserva tiene que seguir puesta.
|
|
134
|
+
|
|
135
|
+
- **Un veredicto se lee de una sola forma, así que las dos cuentas de un registro coinciden.** El registro
|
|
136
|
+
de una evaluación transcribe la respuesta literal del cargo —es la evidencia de la corrida—, así que su
|
|
137
|
+
cuerpo puede traer cualquier cosa, incluida una línea con forma de veredicto. La cuenta de la última
|
|
138
|
+
corrida las tomaba todas y la cuenta compuesta sólo las que cuelgan de un `### <caso>`: dos números
|
|
139
|
+
distintos sobre el mismo archivo y ninguna forma de saber cuál valía.
|
|
140
|
+
|
|
141
|
+
Con un registro de dos casos cuya respuesta citaba un veredicto ajeno, la última corrida decía «2 de 3»
|
|
142
|
+
sobre una corrida que midió dos y pasó las dos. Ahora un veredicto es un caso con su línea —sin
|
|
143
|
+
encabezado no hay a qué atribuirlo— y las dos lecturas salen del mismo lugar.
|
|
144
|
+
|
|
145
|
+
- **Un `agents fork` que se corta a la mitad se retira en vez de quedarse puesto.** Si el copiado fallaba
|
|
146
|
+
—un archivo ilegible, disco lleno— quedaba medio cargo en `agents/`, y lo caro no es perder la copia: es
|
|
147
|
+
dejarla. El catálogo pasa a resolver el slug contra esa copia, así que el intento siguiente ya no dice
|
|
148
|
+
que se cortó sino **«ya lo mantiene esta empresa»** — y lo que la empresa mantiene es un contrato
|
|
149
|
+
incompleto que ningún manifiesto registra, o sea que el aviso de deriva tampoco lo mira nunca.
|
|
150
|
+
|
|
151
|
+
Verificado provocando el corte con un archivo ilegible: antes el segundo intento contestaba «ya lo
|
|
152
|
+
mantiene esta empresa» con `.cauce` vacío; ahora no queda nada en `agents/` y arreglando lo que lo
|
|
153
|
+
cortó el fork sale entero. Sólo se borra lo que esa llamada creó, y la ruta se comprueba contra la raíz
|
|
154
|
+
de la instancia antes de tocarla.
|
|
155
|
+
|
|
156
|
+
- **`automation uninstall` no borra nada si no puede leer la configuración de tu runner.** Borraba
|
|
157
|
+
primero y leía el `settings.json` al final, con un `JSON.parse` sin proteger. Con un archivo que alguien
|
|
158
|
+
dejó a medio fusionar, el comando moría con «Expected property name or '}' in JSON at position 13»
|
|
159
|
+
—que no nombra ni un archivo— **después** de haber borrado los workflows y los cargos: la instancia
|
|
160
|
+
quedaba a medio desinstalar, con la configuración registrando guards que ya no existen.
|
|
161
|
+
|
|
162
|
+
Verificado sobre un banco: el comando se para antes de tocar el disco, dice qué archivo no pudo leer, y
|
|
163
|
+
arreglando ese archivo vuelve a correr.
|
|
164
|
+
|
|
165
|
+
- **Un adaptador propio que no trae `components` ya no rompe el borrador.** El README declara el contrato
|
|
166
|
+
de un adaptador por sus tres funciones y no dice qué campos trae un item, así que un adaptador de la
|
|
167
|
+
empresa —que es lo que ese README invita a escribir— puede no traerlo. Con `serviceFrom: "component"`,
|
|
168
|
+
que es lo que trae el molde, eso era un `TypeError` sobre `undefined` sin nada que lo atribuyera.
|
|
169
|
+
|
|
170
|
+
Sin el campo la respuesta es la que el borrador ya sabía dar: `service: ""` y «Debe definirse un
|
|
171
|
+
servicio único para la promoción». Lo que se toleró es la ausencia y no el caso de uso: con un
|
|
172
|
+
componente sigue resolviendo el servicio y con dos sigue sin resolverlo.
|
|
173
|
+
|
|
174
|
+
- **`promotionEpic` se valida también cuando la promoción es una épica.** El campo se exigía `NNN` sólo
|
|
175
|
+
para una historia. Para una épica es opcional —sin él el número lo elige el motor—, y por eso nadie lo
|
|
176
|
+
miraba; pero cuando está se usa tal cual para armar el nombre del archivo. O sea que el único caso sin
|
|
177
|
+
validar era el único que escribe una ruta con lo que diga el campo.
|
|
178
|
+
|
|
179
|
+
`promotionEpic: 7` producía `epic-7-…`, que el roadmap no reconoce como épica: la promoción se anunciaba
|
|
180
|
+
bien y lo escrito quedaba invisible para la cola. Y con un `..` la épica salía de `roadmap/` entera.
|
|
181
|
+
Verificado sobre una instancia desechable: `integration check` daba exit 0 y `integration promote`
|
|
182
|
+
devolvía «✓ jira:DEMO-42 promovido como epic» dejando el archivo fuera del roadmap. Ahora `check` lo
|
|
183
|
+
rechaza con «promotionEpic debe ser NNN» y `promote`, que valida antes de escribir, no llega a tocar el
|
|
184
|
+
disco.
|
|
185
|
+
|
|
186
|
+
- **El aviso de commits sin entrada de DONE vuelve a decir lo que ve.** El filtro por fecha leía la
|
|
187
|
+
columna del `git log` en una posición fija, y `%h` no mide siempre lo mismo: git sube el largo del hash
|
|
188
|
+
abreviado solo cuando el repositorio crece. En cuanto pasa de siete, la posición fija lee un espacio en
|
|
189
|
+
vez de la fecha y **ningún commit pasa el filtro**, así que el aviso quedaba mudo sin decir por qué —la
|
|
190
|
+
peor forma de que una puerta falle, porque se lee igual que «no hay nada que avisar»—. Ahora se parte
|
|
191
|
+
por espacios y se lee el campo.
|
|
192
|
+
|
|
193
|
+
Verificado en este repositorio, donde el abreviado mide ocho: el aviso pasó de cero a «66 commit(s)
|
|
194
|
+
desde 2026-09-15 que ninguna entrada de DONE nombra». La prueba nueva lo fija en 7, 8 y 12.
|
|
195
|
+
|
|
196
|
+
- **El sello de un registro de evaluación se escribe en el frontmatter y no en el cuerpo.**
|
|
197
|
+
`markConsolidated` buscaba `status:` en el documento entero. Un registro nace sin `status` en su
|
|
198
|
+
frontmatter y su cuerpo transcribe la respuesta literal del cargo: si esa respuesta traía una línea
|
|
199
|
+
`status: <algo>` en la columna cero, el sello aterrizaba ahí y el frontmatter quedaba sin sellar.
|
|
200
|
+
|
|
201
|
+
Un registro sin sellar vuelve a entrar en la propuesta siguiente, o sea que el mismo hallazgo llega dos
|
|
202
|
+
veces — que es exactamente lo que sellar existe para evitar.
|
|
203
|
+
|
|
204
|
+
- **`ops -h` pide la ayuda, y un nombre heredado de `Object` ya no pasa por comando.** Dos huecos de la
|
|
205
|
+
puerta de entrada del CLI, los dos de la misma forma: leer algo que no era ni un comando ni una bandera.
|
|
206
|
+
|
|
207
|
+
`-h` se anunciaba en el uso y no existía: el parser sólo reconoce lo que empieza con `--`, así que caía
|
|
208
|
+
de argumento posicional y `ops check -h` contestaba «no existe el planning en …/-h» **saliendo con 0**.
|
|
209
|
+
Pedir ayuda y recibir un error sobre un directorio inventado es la peor forma de contestar, porque
|
|
210
|
+
parece que el comando corrió.
|
|
211
|
+
|
|
212
|
+
Y la tabla de comandos se consultaba con `FLAGS[comando]`, que resuelve contra `Object.prototype`:
|
|
213
|
+
`ops constructor` salía con 0 sin hacer nada y `ops toString --json` moría con
|
|
214
|
+
«FLAGS[command].includes is not a function». No es un nombre exótico — es lo que sale de pasarle al CLI
|
|
215
|
+
una variable que vino vacía o mal leída desde un script.
|
|
216
|
+
|
|
217
|
+
- **El tope de horas de una propuesta deja de desaparecer cuando la configuración no lo declara.** La
|
|
218
|
+
validación de propuestas lee `runner.maxTaskHours` de `ops.config.json` y tenía un default de cuatro
|
|
219
|
+
horas para cuando no se puede leer. Ese default sólo cubría el archivo ilegible: un `runner` sin el
|
|
220
|
+
campo no lanza nada, así que el tope quedaba en `undefined` y la comparación pasaba a ser falsa
|
|
221
|
+
siempre. O sea que el límite no se aflojaba: se iba entero, y en silencio.
|
|
222
|
+
|
|
223
|
+
Se nota poco a propósito de lo que es — lo que deja de pasar es un **rechazo**—, así que una propuesta
|
|
224
|
+
de cuarenta horas viajaba a Jira sin que nada la mirara. Verificado sobre una configuración con
|
|
225
|
+
`runner: {}`: antes no salía ni un error, ahora sale «supera 4h; debe dividirse o justificarlo».
|
|
226
|
+
|
|
227
|
+
Si tu `ops.config.json` declara `maxTaskHours`, nada cambia — `check` ya lo exigía mayor que cero.
|
|
228
|
+
|
|
229
|
+
- **`rm -r` sobre la raíz o el home se frena también entrecomillado.** La regla reconocía el destino sólo
|
|
230
|
+
desnudo: `rm -rf /` bloqueaba y `rm -rf "/"`, `rm -rf "$HOME"`, `rm -rf ${HOME}`, `rm -rf $HOME/` y
|
|
231
|
+
`rm -rf -- /` pasaban. La comilla es lo que más importa — citar una variable es la forma *correcta* de
|
|
232
|
+
escribirlo en bash, así que el hueco premiaba al que tiene el hábito bueno.
|
|
233
|
+
|
|
234
|
+
Los borrados con destino concreto siguen pasando, también entrecomillados: se comprobó con
|
|
235
|
+
`rm -rf "$HOME/proyecto/dist"` entre otros cinco.
|
|
236
|
+
|
|
237
|
+
- **Un commit escrito en dos líneas vuelve a pasar por los guards.** `isCommit` reconocía el commit
|
|
238
|
+
después de `;`, `&` o `|` y no después de un salto de línea ni dentro de un subshell — o sea que
|
|
239
|
+
`git add x` ⏎ `git commit -m x` en un solo Bash, que es como se escriben dos pasos, dejaba sin correr
|
|
240
|
+
`governance`, `dependencies` y `verify`, y también el bloqueo que impide stagear y commitear a la vez.
|
|
241
|
+
|
|
242
|
+
`bash -c "git commit …"` sigue sin reconocerse, a propósito: hacerlo exige desentrecomillar, y eso
|
|
243
|
+
choca con que el mensaje de un commit sea dato y no código.
|
|
244
|
+
|
|
245
|
+
- **Redirigir con `>|` ya no evade el guard que mira a dónde escribe un comando.** `>|` es el override de
|
|
246
|
+
`noclobber` y escribe igual que `>`, pero el guard no veía su destino: el comando se partía en segmentos
|
|
247
|
+
por `|` antes de buscar destinos, así que la redirección quedaba separada de su ruta. Verificado sobre
|
|
248
|
+
un banco: `echo x > <fuera>` y `echo x >> <fuera>` bloqueaban y `echo x >| <fuera>` pasaba.
|
|
249
|
+
|
|
250
|
+
Importa porque una de las rutas que se escriben por ese hueco es `planning/.ops-approval` — la
|
|
251
|
+
aprobación que después habilita el push a la rama viva—, que es lo que cerraron el 098 y el 119.
|
|
252
|
+
|
|
253
|
+
Las tuberías corrientes siguen sin producir destino: se comprobó con `ls | wc -l` y `a || b`.
|
|
254
|
+
|
|
17
255
|
## [0.94.0] - 2026-09-16
|
|
18
256
|
|
|
19
257
|
### Agregado
|
|
@@ -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
|
})
|
|
@@ -22,6 +22,12 @@ const { hasHooks } = require('./config')
|
|
|
22
22
|
|
|
23
23
|
function check(root) {
|
|
24
24
|
const errors = []
|
|
25
|
+
// La configuración se lee primero y se corta ahí. La resolución del paquete cuelga de ella —`declaredRoot`
|
|
26
|
+
// la parsea— así que un JSON roto se propagaba como archivos del motor que faltan, y el aviso mandaba a
|
|
27
|
+
// correr `npm install` sobre un motor que está instalado: la acción sugerida no arreglaba nada. Nombrar
|
|
28
|
+
// la causa y no seguir es lo que separa este aviso del ruido — enumerar ausencias que salen de una causa
|
|
29
|
+
// ya nombrada manda a arreglar lo que no está roto (caso 174).
|
|
30
|
+
try { O.mode(root) } catch (error) { return [error.message] }
|
|
25
31
|
const hookDir = path.join(root, 'automatization', 'hooks')
|
|
26
32
|
if (!fs.existsSync(path.join(root, 'automatization', 'AGENTS.md'))) {
|
|
27
33
|
errors.push('falta automatization/AGENTS.md')
|
|
@@ -52,9 +58,9 @@ function check(root) {
|
|
|
52
58
|
}
|
|
53
59
|
// Un choque que `upgrade` conservó (caso 110) también queda distinto del paquete, y mandarlo a correr
|
|
54
60
|
// `upgrade` era una vuelta sin salida: lo conservaría otra vez. Se dice qué es y qué hacer.
|
|
55
|
-
const
|
|
61
|
+
const collisions = new Set(O.collisions(root))
|
|
56
62
|
for (const { file, edited } of staleHooks(root)) {
|
|
57
|
-
if (
|
|
63
|
+
if (collisions.has(`automatization/hooks/${file}`)) {
|
|
58
64
|
errors.push(`automatization/hooks/${file}: es tuyo y se llama como uno que trae el paquete, así que el `
|
|
59
65
|
+ "del paquete no está instalado; renombrá el tuyo y corré `cauce upgrade`")
|
|
60
66
|
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 = {
|
|
@@ -45,9 +45,9 @@ function probeBridge(paths, runner) {
|
|
|
45
45
|
const problems = []
|
|
46
46
|
// Desde la raíz y desde una carpeta de adentro: si el runner lanza el hook con otro cwd, la ruta
|
|
47
47
|
// relativa de su configuración deja de resolver y eso hay que verlo acá, no en la primera sesión.
|
|
48
|
-
const
|
|
48
|
+
const launchDirs = [paths.install, path.dirname(script)]
|
|
49
49
|
for (const event of hookEvents) {
|
|
50
|
-
for (const cwd of
|
|
50
|
+
for (const cwd of launchDirs) {
|
|
51
51
|
const payload = JSON.stringify({ toolCall: { args: { CommandLine: 'ls', Cwd: paths.install } } })
|
|
52
52
|
const result = spawnSync(process.execPath, [script, event], { cwd, input: payload, encoding: 'utf8' })
|
|
53
53
|
let response = {}
|
|
@@ -75,11 +75,11 @@ function doctor(root, name, output = console) {
|
|
|
75
75
|
const warnings = []
|
|
76
76
|
try {
|
|
77
77
|
const expected = runnerConfig(paths, root)
|
|
78
|
-
const
|
|
79
|
-
if (!includesConfig(
|
|
78
|
+
const installed = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
79
|
+
if (!includesConfig(installed, expected)) {
|
|
80
80
|
errors.push(`${runner.config.target}: configuración instalada incompleta o divergente`)
|
|
81
81
|
}
|
|
82
|
-
const legacy = legacyGuardWiring(
|
|
82
|
+
const legacy = legacyGuardWiring(installed)
|
|
83
83
|
if (legacy.length) {
|
|
84
84
|
warnings.push(`${runner.config.target}: ${legacy.join(', ')} siguen registrados sueltos junto al grupo; `
|
|
85
85
|
+ 'cada uno se ejecuta dos veces. Borrá esas entradas del archivo para quedarte sólo con el grupo')
|
|
@@ -221,6 +221,18 @@ function uninstall(root, name, output = console) {
|
|
|
221
221
|
}
|
|
222
222
|
const runner = runnerManifest(root, name)
|
|
223
223
|
const paths = runnerPaths(root, name, runner)
|
|
224
|
+
// La configuración se lee **antes** de borrar nada, aunque se use al final. Leyéndola al final, un
|
|
225
|
+
// `settings.json` a medio fusionar hacía morir el comando con el mensaje crudo del parser —que no
|
|
226
|
+
// nombra ni un archivo— con los workflows y los cargos ya borrados: la instancia quedaba a medio
|
|
227
|
+
// desinstalar, con la configuración registrando guards que no existen. Acá no hay nada que deshacer
|
|
228
|
+
// porque todavía no se tocó el disco, que es más barato que cualquier rollback.
|
|
229
|
+
const hasConfig = fs.existsSync(paths.configTarget)
|
|
230
|
+
let config
|
|
231
|
+
if (hasConfig) {
|
|
232
|
+
try { config = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8')) } catch (error) {
|
|
233
|
+
throw new Error(`${runner.config.target} contiene JSON inválido (${error.message})`)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
224
236
|
const prefix = opsPrefix(root)
|
|
225
237
|
const recorded = M.readRunners(root)
|
|
226
238
|
const kept = []
|
|
@@ -266,9 +278,8 @@ function uninstall(root, name, output = console) {
|
|
|
266
278
|
}
|
|
267
279
|
}
|
|
268
280
|
|
|
269
|
-
if (
|
|
270
|
-
const
|
|
271
|
-
const clean = unmergeConfig(unmergeConfig(current, runnerConfig(paths, root)), runner.config.retired || {})
|
|
281
|
+
if (hasConfig) {
|
|
282
|
+
const clean = unmergeConfig(unmergeConfig(config, runnerConfig(paths, root)), runner.config.retired || {})
|
|
272
283
|
if (clean && Object.keys(clean).length) F.atomicWriteJson(paths.configTarget, clean)
|
|
273
284
|
else { removeFile(paths.configTarget, paths.install); removed += 1 }
|
|
274
285
|
output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
|
package/engine/cli/archive.js
CHANGED
|
@@ -11,10 +11,7 @@ const P = require('../planning/parser')
|
|
|
11
11
|
const PC = require('../planning/contracts')
|
|
12
12
|
const AD = require('../planning/adoption')
|
|
13
13
|
const F = require('../core/files')
|
|
14
|
-
const { fail, planningRoot } = require('./io')
|
|
15
|
-
|
|
16
|
-
// La fecha de hoy, la misma que usan los comandos que leen.
|
|
17
|
-
const TODAY = () => new Date().toISOString().slice(0, 10)
|
|
14
|
+
const { fail, planningRoot, TODAY, USAGE, REFUSED } = require('./io')
|
|
18
15
|
|
|
19
16
|
// El historial de acciones humanas se acumula en un solo archivo y no por épica: una fila no pertenece
|
|
20
17
|
// a ninguna, y esperar el cierre de una épica dejaría sin archivar las de un planning que todavía no
|
|
@@ -33,7 +30,7 @@ function adopt(dir) {
|
|
|
33
30
|
const existing = fs.readFileSync(target, 'utf8')
|
|
34
31
|
if (!AD.sealWarnings(root).some((one) => /sin huella/.test(one))) {
|
|
35
32
|
fail(`${AD.BASELINE} ya existe: se genera una vez. Para retirar un renglón, ponele \`#~\` `
|
|
36
|
-
+ 'delante; `check` marca los que ya cumplen.')
|
|
33
|
+
+ 'delante; `check` marca los que ya cumplen.', REFUSED)
|
|
37
34
|
}
|
|
38
35
|
const slugs = AD.declared(existing)
|
|
39
36
|
F.atomicWrite(target, existing.replace(/\n?$/, `\n# huella: ${slugs.length} entradas · `
|
|
@@ -81,7 +78,7 @@ function archiveHumanActions(root) {
|
|
|
81
78
|
function archive(dir, rawNum) {
|
|
82
79
|
if (String(rawNum || '') === 'human-actions') return archiveHumanActions(planningRoot(dir))
|
|
83
80
|
return fail('Sólo se archiva `human-actions`. La evidencia de una tarea ya vive en su propio archivo '
|
|
84
|
-
+ 'de `done/`, así que archivar una épica dejó de tener sentido.',
|
|
81
|
+
+ 'de `done/`, así que archivar una épica dejó de tener sentido.', USAGE)
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
module.exports = { archive, adopt }
|