Files
hammer/scripts/farm/cosecha-cron.sh
T
sergioandClaude Opus 4.8 b08e582c89 catálogo objetivo P4: la cola de la granja pasa a ser derivada del grafo
drenar.py parte la deuda en ONDAS topológicas (una receta sólo espera por deps
que también estén en deuda; las selladas ya están en el store). Onda 1 =
construible ya; dentro de cada onda, orden por `unblocks` desc.

Da lo que una lista plana no da: el orden correcto, la PROFUNDIDAD real de la
cadena (pasos secuenciales = el reloj de pared) y qué paraleliza sin pisarse.

   base                0 en deuda   imagen completa
   cli                 0 en deuda   imagen completa
   escritorio-kde     77 en deuda   12 ondas   onda 1 = 1 receta: qtbase
   escritorio-mirada   4 en deuda    2 ondas   onda 1 = 3

HALLAZGO: el escritorio KDE está serializado detrás de UNA receta. qtbase
destraba 117 nodos y es lo único de la onda 1 — hasta que no esté sellada no hay
nada que paralelizar, por más workers que se enciendan. Cambia la pregunta de
"¿cuántas faltan?" (77, poco informativo) a "¿cuál es el camino crítico?".

Dos correcciones salidas de mirar la salida:
· el grafo se elige por la `cola` declarada en targets.toml, NO por cuál tiene
  más nodos: incoming-kde SOMBREA recetas canónicas, así que medir
  escritorio-mirada contra el grafo KDE medía una imagen que nadie construye
  (3 en deuda en vez de 4). El mismo atajo estaba en seed-graph.py --frontera;
  corregido en los dos.
· regla de la fuente única implementada en deps_de(): manda la receta si existe,
  la semilla sólo si el nodo es `wanted`, marcando procedencia en la salida.

Enchufado al latido: cosecha-cron regenera docs/state/drenaje.json junto al
grafo y lo commitea ⇒ cola derivada y versionada, el git diff entre ciclos
muestra qué salió de la deuda. NO lanza builds: eso cuesta € y sigue siendo
decisión explícita (farm-up + campana-deuda.sh).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 18:14:37 -04:00

106 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# cosecha-cron.sh — el LATIDO de la granja: recoger + sembrar, en bucle, sin agente ni tokens.
#
# QUÉ HACE cada ciclo, por cada worker vivo en scripts/farm/.fleet:
# 1. SIEMBRA (sube): rsync de código + recetas laptop→worker (el laptop es canónico; si autoré
# recetas nuevas —p.ej. openrc— el worker las empieza a moler sin que yo intervenga).
# 2. RECOGE (baja): rsync del store sellado worker→laptop (CAS ⇒ merge seguro, sin conflicto).
# 3. Regenera el GRAFO DE ESTADO (build-state.json + build-state-kde.json) desde el store ya
# cosechado, y commitea SÓLO esos ficheros firmes. El `git log` de docs/state/ ES el avance
# que el humano ve y sigue (huecos, fallos, qué se selló entre dos ciclos).
#
# LO QUE **NO** HACE (a propósito): NO promueve incoming-kde→recipes/ canónico (esa es decisión de
# clasificación humana — "el hub clasifica", ver granja-promote-colisiones) ni firma el índice. Sólo
# captura al store del laptop y deja el veredicto para la mañana. Idempotente y re-corrible.
#
# INFINITO: vive en el crontab hasta que lo saques (`crontab -e`, borrar la línea cosecha-cron).
# Robusto a worker-ausente: si el dead-man ya mató al worker (cola seca ⇒ €0), el paso 1/2 falla
# suave, se loguea y igual regenera+commitea el estado local. NO relanza workers (eso gasta €;
# es decisión explícita con farm-up).
#
# Uso: scripts/farm/cosecha-cron.sh # un ciclo (lo que dispara el cron)
# */30 * * * * cd /home/sergio/hammer && scripts/farm/cosecha-cron.sh >>work/cosecha-cron.log 2>&1
# Env: SSH_KEY (def ~/.ssh/github5), REMOTE (def /opt/hammer), NO_COMMIT=1 (regenera pero no commitea)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
# LOCK (2026-07-22): el latido ya no viene sólo del crontab — `latido.sh` lo lanza desde la sesión
# porque el laptop arranca a veces con arje-zero y a veces con OpenRC, y cronie sólo existe en uno
# de los dos. Con dos disparadores posibles, dos ciclos pueden solaparse: ambos hacen rsync sobre el
# mismo store y, peor, `git commit`+`push` a la vez (index.lock, o un push que pisa al otro). El lock
# va ACÁ y no en el llamador para que valga venga de donde venga: cron, latido o a mano.
# -n = no esperar: si ya hay un ciclo corriendo, este sobra (el próximo tick recoge lo mismo).
LOCKFILE="${LOCKFILE:-$ROOT/work/.cosecha-cron.lock}"
mkdir -p "$(dirname "$LOCKFILE")"
exec 9>"$LOCKFILE"
if ! flock -n 9; then
echo "── $(date -u +%FT%TZ) cosecha-cron: ya hay un ciclo en curso ⇒ salgo (no me solapo)"
exit 0
fi
SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
REMOTE="${REMOTE:-/opt/hammer}"
FLEET="$ROOT/scripts/farm/.fleet"
HAMMER="${HAMMER:-$ROOT/target/release/hammer}"
# Workers efímeros reciclan IPs ⇒ no fijar known_hosts (hub-and-spoke, worker sin secretos).
SSH="ssh -i $SSH_KEY -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o BatchMode=yes -o LogLevel=ERROR -o ConnectTimeout=15"
ts() { date -u +%FT%TZ; }
echo "── $(ts) cosecha-cron arranca"
if [ ! -s "$FLEET" ]; then
echo " flota vacía (scripts/farm/.fleet) — nada que cosechar este ciclo"
else
while read -r name ip; do
[ -n "${name:-}" ] || continue
echo "==> $name ($ip)"
# 1. SIEMBRA: código + recetas laptop→worker (--delete: el laptop manda; nunca toca work/store/target).
if rsync -az --delete -e "$SSH" \
--exclude /work --exclude /store --exclude '/store-*' --exclude /target \
--exclude /dist --exclude /.dev-fs --exclude /.git --exclude /.scratch \
--exclude '*.png' --exclude '/content*' \
./ "root@$ip:$REMOTE/" 2>&1 | tail -2; then
echo " siembra ✓"
else
echo " ⚠ siembra falló (worker ausente/idle-muerto?) — sigo con el siguiente"
continue
fi
# 2. RECOGE: store sellado worker→laptop (CAS, merge seguro).
if rsync -az -e "$SSH" "root@$ip:$REMOTE/store/" "$ROOT/store/" 2>&1 | tail -2; then
echo " cosecha ✓"
else
echo " ⚠ cosecha falló — sigo"
fi
done < "$FLEET"
fi
# 3. Regenerar el grafo de estado desde el store ya cosechado (usa el hammer del laptop, que tiene
# el subcomando `hash`). Barato (~14s cada uno). El corpus canónico y el frente KDE, por separado.
echo "==> regenerando grafo de estado"
if [ -x "$HAMMER" ]; then
scripts/build-state.py >/dev/null 2>&1 && echo " build-state.json ✓" || echo " ⚠ build-state.py falló"
scripts/build-state.py --kde >/dev/null 2>&1 && echo " build-state-kde.json ✓" || echo " ⚠ build-state.py --kde falló"
# Cola DERIVADA del grafo (P4): el orden de masticado por imagen, en ondas topológicas. No lanza
# ningún build — sólo deja escrito qué conviene moler primero, para que la cola deje de ser una
# lista escrita a mano. Barato (lee JSON, no hashea).
scripts/drenar.py --todos >/dev/null 2>&1 && echo " drenaje.json ✓" || echo " ⚠ drenar.py falló"
else
echo " ⚠ sin binario hammer en $HAMMER — no regenero estado"
fi
# 4. Commitear SÓLO el estado firme (el avance que el humano sigue). Recetas nuevas que YO autoré se
# commitean aparte, a mano, para no meter autoría a medias en un commit de cron.
if [ "${NO_COMMIT:-}" != "1" ]; then
git add docs/state/build-state.json docs/state/build-state-kde.json docs/state/drenaje.json 2>/dev/null || true
if ! git diff --cached --quiet 2>/dev/null; then
git commit -q -m "estado: cosecha granja $(ts) — avance del árbol KDE" 2>/dev/null \
&& { git push -q origin main 2>/dev/null && echo "==> estado commiteado+pusheado" \
|| echo "==> estado commiteado (push falló, reintenta próximo ciclo)"; }
else
echo "==> sin cambios de estado este ciclo"
fi
fi
echo "── $(ts) cosecha-cron fin"