> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
harkaq: campaña desatendida — medir→declarar→verificar, sin regex y sin preguntas
El problema real no es el coste de la tanda: es que una pregunta a las 3am son 8
horas de idle. Estas 3 piezas están hechas para que la noche no se desperdicie.
harkaq-campana.sh (worker) bucle autónomo: construye cada receta bajo la
jaula y guarda los veredictos crudos. Idempotente, saltea
lo medido, duerme y re-escanea. NUNCA bloquea esperando: un
build que falla no detiene la cola (y su veredicto interesa
igual — es cuando más importa saber qué tocó fuera). Gatea
por ABI>=7: moler la noche para producir "no sabemos" es
peor que no moler.
harvest-harkaq.sh (hub, cron) hermano de harvest-go.sh: baja veredictos,
clasifica contra el store COMPLETO, aplica las deps
DECLARABLES y commitea con `git add` explícito. La deuda
IRREDUCIBLE NO se toca (declarar algo que el store no
provee rompe el build en vez de arreglarlo) → needs-review.
harkaq-add-deps.py editor de recetas conservador: idempotente, preserva las
deps existentes, y ANTE LA DUDA NO TOCA (valida que el
resultado siga parseando como TOML y no encoja). Corre de
noche sobre la fuente de verdad del catálogo: un fichero mal
editado a las 3am no da un error, da una receta corrupta que
nadie mira hasta el lunes.
POR QUÉ NO HAY REGEX EN EL CAMINO CRÍTICO: intenté sacar la lista de "a quién le
falta declarar make" con uno y falló dos veces en la misma tarde. `\bmake\b`
colaba `cargo-make` (el guión ES límite de palabra). La versión estrecha perdía
LAS CINCO recetas donde harkaq había medido la deuda de verdad (`compile = "make
…"`: el carácter previo es una comilla). El número bailó 69→45→99 según el
retoque. Construí una herramienta de medición y después intenté adivinar con un
regex — la lista buena la da el kernel. El regex queda sólo para ORDENAR la cola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/scripts/farm/harkaq-add-deps.py b/scripts/farm/harkaq-add-deps.py
new file mode 100755
index 00000000..96ce52e2
--- /dev/null
+++ b/scripts/farm/harkaq-add-deps.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""harkaq-add-deps — añade deps de build a una receta, sin romperla. (Cosecha desatendida.)
+
+ harkaq-add-deps.py recipes/zlib.toml make binutils
+
+Idempotente: si la dep ya está, no toca nada y sale 1 (nada que commitear).
+
+Corre DESATENDIDO y de noche, editando ficheros que son la fuente de verdad del catálogo. Por eso
+la regla es: **ante la duda, no tocar**. Un fichero mal editado a las 3am no da un error, da una
+receta corrupta que nadie mira hasta el lunes. Cada caso que no se entiende sale sin cambios y con
+motivo, que es la misma disciplina de `SinEvidencia`: no afirmar lo que no se sabe.
+"""
+import re
+import sys
+
+
+def anadir(path, nuevas):
+ with open(path) as f:
+ s = f.read()
+ orig = s
+
+ m = re.search(r'^\[deps\]\s*$(.*?)(?=^\[|\Z)', s, re.M | re.S)
+ if m:
+ bloque = m.group(1)
+ mb = re.search(r'^(\s*build\s*=\s*)\[([^\]]*)\]', bloque, re.M)
+ if mb:
+ ya = re.findall(r'"([^"]+)"', mb.group(2))
+ falta = [d for d in nuevas if d not in ya]
+ if not falta:
+ return False, "ya estaban declaradas"
+ lista = ya + falta
+ nuevo = mb.group(1) + "[" + ", ".join(f'"{d}"' for d in lista) + "]"
+ s = s[:m.start(1)] + bloque[:mb.start()] + nuevo + bloque[mb.end():] + s[m.end(1):]
+ else:
+ # Hay [deps] pero sin `build = [...]` (p.ej. sólo `runtime`). Insertar la línea.
+ linea = "build = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
+ s = s[:m.start(1)] + "\n" + linea + bloque.lstrip("\n") + s[m.end(1):]
+ else:
+ # Sin [deps]: se añade al final. `[build.phases]` suele ser la última sección y sus strings
+ # multilínea (''' … ''') se comerían un bloque puesto después ⇒ va ANTES de la primera
+ # sección [build...] si existe, y si no, al final.
+ bloque = "\n[deps]\nbuild = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
+ mfase = re.search(r'^\[build\.phases\]', s, re.M)
+ if mfase:
+ s = s[:mfase.start()] + bloque.lstrip("\n") + "\n" + s[mfase.start():]
+ else:
+ s = s.rstrip("\n") + "\n" + bloque
+
+ if s == orig:
+ return False, "sin cambios"
+
+ # Guarda: sólo escribir si el TOML sigue pareciendo un TOML y no perdimos nada. Un parser de
+ # verdad sería mejor, pero tomllib no ESCRIBE y re-emitir con un writer reordenaría el fichero
+ # entero (y con él, comentarios que valen oro en este repo).
+ try:
+ import tomllib
+ tomllib.loads(s)
+ except Exception as e:
+ return False, f"el resultado no parsea como TOML ({e}) — NO se toca"
+ if len(s) < len(orig):
+ return False, "el resultado es más corto que el original — NO se toca"
+
+ with open(path, "w") as f:
+ f.write(s)
+ return True, "ok"
+
+
+def main():
+ if len(sys.argv) < 3:
+ print(__doc__.split("\n")[2].strip(), file=sys.stderr)
+ return 2
+ path, nuevas = sys.argv[1], sys.argv[2:]
+ ok, motivo = anadir(path, nuevas)
+ if not ok:
+ print(f" ·· {path}: {motivo}", file=sys.stderr)
+ return 0 if ok else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/farm/harkaq-campana.sh b/scripts/farm/harkaq-campana.sh
new file mode 100755
index 00000000..07d3d8b3
--- /dev/null
+++ b/scripts/farm/harkaq-campana.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+# harkaq-campana.sh — bucle AUTÓNOMO del worker para la campaña harkaq. Hermano de
+# farm-worker-loop.sh: muele sin esperar a nadie, 24/7, y no pregunta NADA.
+#
+# scripts/farm/harkaq-campana.sh [lista-de-recetas.txt]
+#
+# Mide, no decide: por cada receta la construye bajo la jaula y guarda los veredictos crudos en
+# work/harkaq-campana/<n>.verdicts. La clasificación y la edición de recetas las hace el HUB
+# (harvest-harkaq.sh, por cron), porque el store del worker es parcial y marcaría como irreducible
+# lo que sí es declarable. El worker MIDE, el hub CLASIFICA.
+#
+# Idempotente: lo ya medido se saltea; re-correrlo sólo mide lo nuevo. Cuando no queda nada,
+# duerme y re-escanea — igual que farm-worker-loop.sh.
+#
+# Diseñado para la NOCHE: nunca bloquea esperando una decisión. Un build que falla NO detiene la
+# cola; su veredicto se guarda igual (un build que falla es justo cuando más interesa saber qué
+# tocó fuera de su clausura) y se sigue. Ocho horas de idle por una pregunta es el fallo que este
+# script existe para no tener.
+set -u
+
+HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
+cd "$HAMMER_DIR"
+LISTA="${1:-work/harkaq-cola.txt}"
+OUT="work/harkaq-campana"
+BIN="${HARKAQ_BIN:-/root/.cache/harkaq}"
+IDLE_SLEEP="${IDLE_SLEEP:-300}"
+POR_RECETA="${POR_RECETA:-900}"
+mkdir -p "$OUT"
+
+export HARKAQ=1 HARKAQ_BIN="$BIN" HARKAQ_BASE="${HARKAQ_BASE:-$BIN/base.policy}"
+export HARKAQ_TIMEOUT="${HARKAQ_TIMEOUT:-300}"
+
+# Gate: sin ABI>=7 el kernel no audita y TODOS los veredictos saldrían SinEvidencia. Moler la
+# noche entera para producir "no sabemos" es peor que no moler.
+abi=$(python3 -c 'import ctypes;print(ctypes.CDLL("libc.so.6").syscall(444,None,0,1))' 2>/dev/null)
+if [ "${abi:-0}" -lt 7 ]; then
+ echo "ABORTO: Landlock ABI ${abi:-?} < 7 ⇒ sin audit de denegaciones. ¿La golden es la 6.17?"
+ exit 4
+fi
+echo "$(date -u +%FT%TZ) campaña harkaq: ABI $abi, cola $LISTA"
+
+while :; do
+ hechas=0; nuevas=0
+ while IFS= read -r n; do
+ [ -n "$n" ] || continue
+ [ -f "recipes/$n.toml" ] || continue
+ if [ -s "$OUT/$n.verdicts" ]; then hechas=$((hechas + 1)); continue; fi
+
+ # Forzar build real: `hammer build` pega en la caché si el artefacto ya está sellado y no
+ # correría nada (veredicto vacío que NO es un fallo). La copia va AL LADO del original o se
+ # rompe la resolución de deps.build, que son relativas al dir de la receta.
+ tmp="recipes/.hk-$n.toml"
+ sed "s/^name *= *\"$n\"/name = \"$n-hkm\"/" "recipes/$n.toml" > "$tmp"
+ timeout "$POR_RECETA" ./target/release/hammer build "$tmp" --store "$PWD/store" \
+ > "$OUT/$n.log" 2>&1
+ rm -f "$tmp"
+ grep '^\[harkaq\] {' "$OUT/$n.log" | sed 's/^\[harkaq\] //' > "$OUT/$n.verdicts"
+ if [ -s "$OUT/$n.verdicts" ]; then
+ nuevas=$((nuevas + 1))
+ echo "$(date -u +%FT%TZ) ++ $n ($(wc -l < "$OUT/$n.verdicts") fase/s medidas)"
+ else
+ # Sin veredicto: el build ni arrancó (fetch, dep rota…). Se marca para no reintentar
+ # en bucle y se SIGUE — la cola no se detiene por una receta.
+ echo "sin-veredicto" > "$OUT/$n.skip"
+ rm -f "$OUT/$n.verdicts"
+ echo "$(date -u +%FT%TZ) ?? $n — sin veredicto, salteada (ver $OUT/$n.log)"
+ fi
+ done < "$LISTA"
+
+ echo "$(date -u +%FT%TZ) ciclo: $nuevas nuevas, $hechas ya medidas. durmiendo ${IDLE_SLEEP}s"
+ sleep "$IDLE_SLEEP"
+done
diff --git a/scripts/farm/harvest-harkaq.sh b/scripts/farm/harvest-harkaq.sh
new file mode 100755
index 00000000..1e25b7ff
--- /dev/null
+++ b/scripts/farm/harvest-harkaq.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+# harvest-harkaq.sh — COSECHA-ONLY de la campaña harkaq (modo desatendido, para cron en el hub).
+# Determinista, sin IA. Hermano de harvest-go.sh y con la misma disciplina: no elige, no diagnostica,
+# no compila en el hub; sólo convierte en commit lo que el worker YA midió.
+#
+# Uso (cron): cd /home/sergio/hammer && scripts/farm/harvest-harkaq.sh root@<ip> >>work/harvest-harkaq.log 2>&1
+#
+# POR QUÉ EXISTE ASÍ: la lista de "recetas a las que les falta declarar make" NO se puede sacar de
+# un regex. Se intentó y falló dos veces en la misma tarde: `\bmake\b` colaba `cargo-make` (el guión
+# es límite de palabra) y la versión estrecha perdía LAS CINCO recetas donde harkaq había medido la
+# deuda de verdad (`compile = "make …"`: el carácter previo es una comilla). El número bailó 69→45→99
+# según el retoque. La fuente de verdad es la MEDICIÓN, no la conjetura — que es exactamente para lo
+# que existe harkaq. Este script sólo aplica lo que el kernel denunció.
+#
+# Ciclo (idempotente, seguro de re-correr):
+# 1. git pull --rebase (otro agente pushea al mismo main)
+# 2. baja los veredictos del worker
+# 3. por cada veredicto: harkaq-suggest contra el store COMPLETO del hub (el del worker es parcial
+# y marcaría como irreducible lo que sí es declarable — el worker MIDE, el hub CLASIFICA)
+# 4. si la deuda es DECLARABLE ⇒ añade la dep a la receta. Si es IRREDUCIBLE ⇒ NO la toca: va a
+# tandas/needs-review-harkaq/ con el motivo. Declarar algo que el store no provee rompería el
+# build en vez de arreglarlo.
+# 5. commit + push con `git add` EXPLÍCITO (nunca -A: respeta al otro agente)
+set -u
+
+HUB="$(cd "$(dirname "$0")/../.." && pwd)"
+cd "$HUB"
+WORKER="${1:?uso: harvest-harkaq.sh root@<ip>}"
+SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}"
+REMOTE="${REMOTE:-/opt/hammer}"
+SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes"
+REVIEW="tandas/needs-review-harkaq"
+mkdir -p "$REVIEW" work
+
+echo "═══ $(date -u +%FT%TZ) cosecha harkaq desde $WORKER"
+git pull --rebase -q 2>/dev/null || echo " (pull falló; sigo con lo local)"
+
+VERD=work/harkaq-verdicts
+rm -rf "$VERD"; mkdir -p "$VERD"
+rsync -az -e "$SSH" "$WORKER:$REMOTE/work/harkaq-campana/" "$VERD/" 2>/dev/null || {
+ echo " sin veredictos que cosechar (¿el worker sigue midiendo?)"; exit 0; }
+
+tocadas=""
+for v in "$VERD"/*.verdicts; do
+ [ -f "$v" ] || continue
+ n=$(basename "$v" .verdicts)
+ [ -f "recipes/$n.toml" ] || continue
+
+ deps=""; irr=0
+ while IFS= read -r j; do
+ [ -n "$j" ] || continue
+ echo "$j" > /tmp/.hkv.json
+ s=$(scripts/harkaq/harkaq-suggest.py /tmp/.hkv.json "$HOME/.cache/harkaq/base.policy" \
+ "$HUB/store" "$HUB/recipes" 2>/dev/null) || true
+ echo "$s" | grep -q IRREDUCIBLE && irr=1
+ # "→ declarar dep: X" (el store lo provee y sabe cuál artefacto es).
+ d=$(echo "$s" | sed -n 's/.*→ declarar dep: \([a-zA-Z0-9._-]*\).*/\1/p')
+ deps="$deps $d"
+ done < "$v"
+
+ deps=$(echo "$deps" | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ')
+ [ -n "$deps" ] || continue
+
+ if [ "$irr" = 1 ]; then
+ # NO se toca: declarar algo que el store no provee rompe el build en vez de arreglarlo.
+ cp "$v" "$REVIEW/$n.verdicts"
+ echo " ?? $n — deuda irreducible ⇒ a needs-review (deps declarables vistas:$deps)"
+ continue
+ fi
+
+ if python3 scripts/farm/harkaq-add-deps.py "recipes/$n.toml" $deps; then
+ echo " ++ $n — declaradas:$deps"
+ tocadas="$tocadas recipes/$n.toml"
+ fi
+done
+
+if [ -n "$tocadas" ]; then
+ # `git add` EXPLÍCITO por rutas: el repo lo comparte otro agente (cola gráfica/Go).
+ # shellcheck disable=SC2086
+ git add $tocadas
+ git commit -q -m "harkaq: declarar deps medidas por el kernel (cosecha automática)
+
+Cosecha desatendida de la campaña harkaq. Cada dep de acá la denunció el audit de
+Landlock en un build real: el kernel vio al build usarla sin declararla, y el
+store del hub confirma qué artefacto la provee (deuda DECLARABLE, §4.5).
+
+Recetas:$(echo "$tocadas" | tr ' ' '\n' | grep -v '^$' | sed 's|recipes/||;s|\.toml||' | tr '\n' ' ')
+
+No se tocó ninguna receta con deuda IRREDUCIBLE: declarar algo que el store no
+provee rompería el build en vez de arreglarlo. Esas van a $REVIEW.
+
+Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" && git push -q 2>/dev/null || {
+ git pull --rebase -q && git push -q; }
+ echo " ✓ commit+push de $(echo "$tocadas" | wc -w) receta(s)"
+else
+ echo " nada que declarar en este ciclo"
+fi