feat(granja): el latido dice QUÉ deuda bloquea la clausura y con qué comando se salda
«cierre de IMAGEN por perfil: 3 de 10 con clausura completa» es una medición, no un encargo, y así
estuvo durante días sin que nadie la convirtiera en trabajo. Medido hoy: los SIETE perfiles
incompletos lo estaban por las MISMAS 7 recetas en deuda del corpus, y `wireplumber` llevaba ocho
días —desde que una dep le movió el hash el 07-oct— bloqueando `escritorio-kde` y `escritorio-sway`.
Ninguna tenía quien la construyera: el bucle del worker (`takana-farm.service`) sólo muele
`recipes/incoming/`, y la deuda del CORPUS se salda con `campana-deuda.sh`, que es manual **a
propósito** —el store del worker es parcial, así que si midiera la deuda él mismo se pondría a
reconstruir media distro (está en su cabecera desde el 2026-07-21)—. O sea que el eslabón que
faltaba no era una herramienta: era que el latido, que ya tiene la medición, dijera la lista y el
comando.
Sale sólo cuando hay algo que hacer, ordenado por cuántos perfiles destraba cada receta (la primera
es la que más paga), y avisa si `cierre-perfiles.py` recortó su lista de impedidos al tope de 10 —un
recorte callado se lee como «lo cubrí todo»—. Probado en los cuatro casos: deuda real, todo en
clausura (muda), recorte simulado e informe ausente.
Con la deuda de hoy imprime:
⇒ 7 receta(s) en deuda bloquean la clausura de 7 perfil(es):
shuma-pregunta destraba 5 perfil(es): …
shuma-daemon destraba 3 perfil(es): …
Nadie las construye solo (el bucle del worker muele incoming/, no el corpus):
ssh root@<worker> "systemd-run --unit=campana-deuda … campana-deuda.sh shuma-pregunta …"
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Semantic delta
No definition changed: this change is text, documentation or data.
1 files · +49 −0
- scripts/farm/cosecha-cron.sh +49 −0
diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh
index 32b2ea51..70c0810a 100755
--- a/scripts/farm/cosecha-cron.sh
+++ b/scripts/farm/cosecha-cron.sh
@@ -459,6 +459,55 @@ if [ -x "$HAMMER" ]; then
echo " cierres.json ✓ $(sed -n '1s/^== //;1s/ ==$//;1p' work/.cierres.salida)"
grep -c "SE MOVIÓ" work/.cierres.salida >/dev/null 2>&1 && \
sed -n '/SE MOVIÓ/p' work/.cierres.salida | sed 's/^ / /' | head -6
+
+ # ── Y LO QUE HAY QUE HACER CON ESE NÚMERO ────────────────────────────────────────────────────
+ # «3 de 10 con clausura completa» es una medición, no un encargo, y así estuvo durante días sin
+ # que nadie la convirtiera en trabajo. Medido el 2026-10-15: los SIETE perfiles incompletos lo
+ # estaban por las MISMAS 7 recetas en deuda del corpus —`wireplumber` llevaba 8 días, desde que
+ # una dep le movió el hash el 07-oct—, y ninguna tenía quien la construyera: el bucle del worker
+ # (`takana-farm.service`) sólo muele `recipes/incoming/`, y la deuda del CORPUS se salda a mano
+ # con `campana-deuda.sh`, que es manual **a propósito** (el store del worker es parcial: si
+ # midiera la deuda él mismo se pondría a reconstruir media distro — ver su cabecera).
+ #
+ # O sea que el eslabón que faltaba no era una herramienta: era que el latido, que ya tiene la
+ # medición, dijera la lista y el comando. Sale sólo cuando hay algo que hacer.
+ python3 - docs/state/cierres.json <<'PY' || true
+import json, sys
+try:
+ d = json.load(open(sys.argv[1]))
+except Exception:
+ sys.exit(0)
+culpables, perfiles, recortados = {}, 0, 0
+for p in d.get('perfiles', []):
+ imp = p.get('impedidos') or []
+ if not imp:
+ continue
+ perfiles += 1
+ # `cierre-perfiles.py` guarda a lo sumo 10 impedidos por perfil y el total aparte. Si recortó,
+ # esta lista cuenta de menos y hay que decirlo: un recorte callado se lee como «lo cubrí todo».
+ if (p.get('n_impedidos') or len(imp)) > len(imp):
+ recortados += 1
+ for linea in imp:
+ ruta = linea.split(' ')[0]
+ cola, _, nombre = ruta.partition('/')
+ culpables.setdefault((cola, nombre), set()).add(p['perfil'])
+if not culpables:
+ sys.exit(0)
+# Orden por cuántos perfiles destraba cada una: la primera es la que más paga.
+orden = sorted(culpables, key=lambda k: (-len(culpables[k]), k[1]))
+print(' ⇒ %d receta(s) en deuda bloquean la clausura de %d perfil(es):' % (len(orden), perfiles))
+for cola, nombre in orden[:10]:
+ print(' %-22s destraba %d perfil(es): %s'
+ % (nombre, len(culpables[(cola, nombre)]), ', '.join(sorted(culpables[(cola, nombre)]))))
+delcorpus = [n for c, n in orden if c == 'corpus']
+if recortados:
+ print(' ⚠ %d perfil(es) traen MÁS impedidos de los que el informe guarda (tope de 10):'
+ ' la lista de arriba cuenta de menos.' % recortados)
+if delcorpus:
+ print(' Nadie las construye solo (el bucle del worker muele incoming/, no el corpus):')
+ print(' ssh root@<worker> "systemd-run --unit=campana-deuda --working-directory=/opt/takana \\')
+ print(' /opt/takana/scripts/farm/campana-deuda.sh %s"' % ' '.join(delcorpus[:10]))
+PY
else
echo " ⚠ cierre-perfiles falló — NO piso el informe anterior"
tail -2 work/.cierres.salida 2>/dev/null | sed 's/^/ /'