Commit Graph
58 Commits
Author SHA1 Message Date
SergioandClaude Opus 4.8 64894d863c bootstrap: builder ejecutable — toolchain con bwrap/git/curl + shim del loader
Correr el rebuild EN la VM destapó dos faltantes reales del builder (justo lo que
la verificación de Stage 2 debe cazar):

1. El toolchain (variante a, desde Alpine) traía compiladores (cargo/make/zig)
   pero NO las herramientas de orquestación del lab — bwrap (anida el sandbox),
   git (git archive del mirror) y curl (tarballs). En el host las aporta el
   sistema; en la VM el toolchain es el único userland capaz, así que deben vivir
   ahí. bootstrap-devfs.sh las añade (paquete `bubblewrap`, no `bwrap`).

2. Esos binarios son Alpine *dinámicos* y no corren desde el userland Stage 1
   *estático* (musl --disable-shared ⇒ no hay loader en /lib). rebuild-stage1
   ahora instala un shim del loader musl (cp a /lib) + LD_LIBRARY_PATH/PATH al
   toolchain antes de invocar hammer; el sandbox de build sigue anidando en
   /toolchain. El hammer estático corre por ruta absoluta.

El builder boot end-to-end ya estaba probado; esto lo hace además *capaz de
reconstruirse*. 28 tests verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:13:32 +00:00
SergioandClaude Opus 4.8 4a35dfb398 docs: runbook §8c — boot del builder confirmado end-to-end en QEMU
Smoke test real (qemu -cpu Broadwell, initramfs 410 MB gz): el builder rootfs
bootea hasta la shell. arje-zero como PID 1 carga+valida la seed card, instancia
hammerd (pid 60) + console-getty (pid 61), hammerd levanta watcher fanotify +
agent.sock, y la getty deja `~ #` en consola. Mismo boot que Stage 1 pero con el
toolchain adentro — listo para `rebuild-stage1`.

Actualiza la receta de §8c: script boot-builder-vm.sh, MEM=6144, kernel real,
nota de RAM (tmpfs + rebuild en RAM va justo sin KVM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:03:30 +00:00
SergioandClaude Opus 4.8 66d4df4f57 bootstrap: script de boot del builder en QEMU (rebuild in-rootfs)
scripts/boot-builder-vm.sh empaqueta el último eslabón operacional del SDD 11 §7:
bootea work/builder.cpio.gz (el builder rootfs como initramfs cpio+gzip) con
arje-zero como PID 1; en la consola se corre `rebuild-stage1` para reconstruir
stage1' con el toolchain de adentro y comparar con la referencia.

- Defaults: KERNEL=/boot/vmlinuz-linux, MEM=6144, CPU=Broadwell (AVX2 bajo TCG).
- KVM=1 usa -enable-kvm -cpu host si hay /dev/kvm (mucho más rápido).
- Documenta la tensión de RAM: el rootfs vive en tmpfs (~1.2 GB) + el rebuild Rust
  necesita varios GB más, todo en RAM; con host ~7 GB y sin KVM va justo/lento.

El initramfs (410 MB gz, 26776 entradas) se verificó: sbin/init→arje-zero, el
hammer estático, rebuild-stage1, rebuild.env y el toolchain/cargo presentes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:01:11 +00:00
SergioandClaude Opus 4.8 83d4b2aa17 bootstrap: builder rootfs — fixes destapados al armarlo con caché real
Al construir el hammer estático musl y armar el builder con --work-cache work,
dos problemas reales (cazados antes de llenar el disco):

1. Recursión. --work-cache work --out work/builder-rootfs deja el destino DENTRO
   de la caché: copiar la caché se tragaba el propio builder a medio armar
   (work/builder-rootfs/work/builder-rootfs/… sin fondo). Fix: link_or_copy_tree
   toma un `skip` (path canónico a no descender), que corta el ciclo.

2. Bloat de sources. --work-cache copiaba TODO work/, incluidos los árboles ya
   materializados de work/sources (4 GB, regenerables). Fix: sólo se embeben
   work/repos (mirrors git) y work/tarballs (descargas verificadas); de ahí
   `fetch` rematerializa los sources en la VM. El builder pasa de ~5 GB a ~750 MB.

+2 tests (recursión termina y no se auto-copia; sources/ no viaja). Runbook §8c
aclara la semántica de --work-cache. El hammer estático musl
(target/x86_64-unknown-linux-musl/release/hammer) sale static-pie linked, listo
para bootear como PID-algo en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:52:49 +00:00
SergioandClaude Opus 4.8 f17a8fa301 bootstrap: builder rootfs — ensambla el rebuild in-rootfs (Stage 2 pleno, §7)
El único sub-ítem que le quedaba a Stage 2: el rebuild *dentro* del rootfs. El
Stage 1 que booteamos es runtime (musl+busybox+hammerd+arje-zero), sin compilador
— no puede reconstruirse. El builder rootfs es Stage 1 + el toolchain adentro.

Implementa `hammer_bootstrap::builder_rootfs` (variante a pragmática, SDD 11 §7.2):
sobre el Stage 1 rootfs hidratado monta /toolchain (rootfs Alpine = sandbox de
build), /store con la semilla replicada (el hammer de adentro resuelve zig por
hash), /usr/bin/hammer + /etc/hammer/recipes, y el driver /usr/bin/rebuild-stage1
que apunta HAMMER_ROOTFS=/toolchain, corre `bootstrap stage1` y compara stage1'
contra la referencia con `stage2 --verify` (lee SEED_HASH/SEED_KIND/REF_CONTENT
de /etc/hammer/rebuild.env).

- BuilderSpec/BuilderReport + builder_hash lógico (insumos: stage1+semilla+
  recetas+binario+tag toolchain+driver) — reproducible y auditable sin hashear el
  árbol Alpine; anota línea stage 2 en el manifiesto.
- link_or_copy_tree (hardlink-or-copy, sin chmod: no toca permisos del .dev-fs).
- El builder no se sella (toolchain Alpine no es content-addressed); se ensambla
  en out_dir para empaquetar como initramfs.
- CLI `hammer bootstrap builder --stage1 H --seed-hash H [--hammer-bin] [--toolchain]
  [--ref-content] [--work-cache] [--out]`. +4 tests (assemble, hash determinista,
  sin-ref, stage1 no sellado).

Validado contra el store real: Stage 1 73d7a9be… + semilla 3ce721ec… ⇒ builder
81dad3d9… (1.2 GB con el toolchain). Lo que queda es operacional: bootear en la
VM y correr rebuild-stage1 — runbook §8c documenta la receta (hammer estático
musl, initramfs, qemu -cpu Broadwell, --work-cache para rebuild offline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:32:14 +00:00
SergioandClaude Opus 4.8 731d4f6ffc docs: SDD 11 §7 — diseño del builder rootfs (camino a Stage 2 pleno)
La verificación de Stage 2 (of_tree + verify_against) está probada y los 4
componentes reconstruyen bit-idéntico. El rebuild *dentro* del rootfs exige un
builder rootfs: el Stage 1 mínimo es runtime, sin compilador.

Diseña: qué necesita el builder (hammer estático + recetas + seed zig + make/
autotools + cargo/rust + linux-headers + bwrap para el sandbox anidado); dos
niveles de pureza fasables — (a) pragmático (toolchain desde Alpine, demuestra el
mecanismo + cierra la reproducibilidad end-to-end) y (b) auto-alojamiento puro
(toolchain construido por hammer desde fuente, incremental); y el enganche con
stage2 (ya ancla of_tree(stage1); el builder produce stage1').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:48:50 +00:00
SergioandClaude Opus 4.8 cc2969f9e6 docs: arje-zero reproducible verificado (lock determinista) — 4/4 componentes
Cierra el ◑ de arje-zero sin un rebuild de horas: su único riesgo era el
Cargo.lock generado sin --locked (tawasuyu lo gitignora). Verificado barato —
dos `cargo generate-lockfile` del commit pinned dan un lock byte-idéntico
(resolución determinista) — y el camino de build Rust ya está probado
bit-reproducible vía hammerd.

Los 4 componentes de Stage 1 (musl, busybox, hammerd, arje-zero) están
verificados reproducibles. Lo único que le queda a Stage 2 es el rebuild dentro
de un builder rootfs (auto-alojamiento pleno) — hito aparte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:47:31 +00:00
SergioandClaude Opus 4.8 d26e38253b docs: hammerd (Rust) verificado bit-reproducible (Stage 2)
Tercer componente probado byte-idéntico entre dos builds independientes: el
camino Rust (crt-static + cargo --locked + SOURCE_DATE_EPOCH + paths fijos /src)
reproduce. 3/4 componentes verificados reproducibles (musl, busybox, hammerd).

arje-zero queda ◑: mismo camino, pero vendorea sin --locked (tawasuyu no committea
Cargo.lock) ⇒ el lock generado podría variar entre corridas; committear el lock
en tawasuyu lo cerraría (plan C.2 #5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:15:28 +00:00
SergioandClaude Opus 4.8 01d5a02b4d bootstrap: busybox reproducible — fixes destapados por la verificación de Stage 2
La verificación de reproducibilidad de Stage 2 (dos builds + diff de bytes) probó
que musl reconstruye bit-idéntico pero busybox NO, y cazó dos no-determinismos:

1. Config interactiva: tras editar .config, silentoldconfig prompea por opciones
   NEW leyendo stdin → set de applets variable (build no-determinista y flaky).
   Fix en busybox.toml: `yes '' | make oldconfig` (defaults, determinista;
   1.36.1 no tiene olddefconfig).
2. Headers UAPI del kernel: el config determinista habilita console-tools, que
   exige linux/kd.h (zig no la bundlea). Fix: linux-headers en bootstrap-devfs.sh
   (zig cc nativo la halla en /usr/include).

Con ambos, musl y busybox reconstruyen bit-idéntico. Runbook §8b documenta la
verificación y deja pendiente: reproducibilidad de los componentes Rust y el
rebuild dentro de un builder rootfs (auto-alojamiento pleno).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:42 +00:00
SergioandClaude Opus 4.8 79ad2cad69 bootstrap: stage2() — ancla y verifica reproducibilidad (pre-Stage 2)
Esqueleto de Stage 2 (SDD 11 §3): la verificación de auto-alojamiento.

- VerifyReport + Reproducibility {Reproducible|Divergent|RebuildPending}
- artifact_content_hash(store, hash, name): of_tree del artefacto sellado
- stage2(stage1, store): ancla el content-hash del rootfs (los BYTES reales, no
  el hash input-addressed del store) como referencia y lo anota en el manifiesto
  (línea stage 2); verdict = RebuildPending
- verify_against(report, rebuilt): compara stage1 vs stage1' → Reproducible/Divergent
- CLI: `hammer bootstrap stage2 --rootfs HASH [--verify CONTENT_HASH]`

Validado sobre el rootfs real: content-hash b3:d0d5669f… (≠ store hash 73d7a9be…,
confirma que of_tree hashea bytes); --verify igual ⇒ ✓ REPRODUCIBLE, distinto ⇒
✗ DIVERGENTE. +2 tests. El rebuild nativo DENTRO del rootfs (produce stage1')
corre en la VM destino — el único sub-ítem que queda de Stage 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:53:34 +00:00
SergioandClaude Opus 4.8 974504e068 build: SOURCE_DATE_EPOCH + TZ=UTC en el sandbox (determinismo pre-Stage 2)
Stage 2 compara el content-hash (of_tree) de stage1 vs stage1' rebuildeado; para
que coincidan, el build debe ser determinista. Las rutas ya lo son (el source se
bindea en /src, constante entre rebuilds); el knob que faltaba es el timestamp:
SOURCE_DATE_EPOCH=1 (+ TZ=UTC) fija los que tar/ar/gzip y __DATE__/__TIME__ embeben.

No cambia ningún ArtifactHash (input-addressed) ⇒ caché intacta; sólo normaliza
los bytes de salida de builds futuros. Plan C.2 #5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:49:35 +00:00
SergioandClaude Opus 4.8 9676fff0dd core: ArtifactHash::of_tree — content-hash determinista de un árbol (pre-Stage 2)
Stage 2 verifica bit-reproducibilidad comparando hash(stage1) vs hash(stage1').
Pero el ArtifactHash del store es INPUT-addressed (Merkle de inputs de receta:
fuente+flags+deps), así que esa comparación sería trivialmente true. Stage 2
necesita un hash de la SALIDA real (los bytes).

of_tree(root) hashea el contenido: rutas relativas ordenadas + tipo + bit de
ejecución + contenido / target de symlink, BLAKE3 length-prefijado. Determinista
e independiente de la ruta raíz y del orden del filesystem; no sigue symlinks.

+2 tests (determinismo entre dos árboles idénticos; detección de cambios de
contenido/exec/symlink-target).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:48:15 +00:00
SergioandClaude Opus 4.8 5a6d188869 docs: CRASHED real demostrado en VM (cierra el diferido de la Fase 5)
Desde la consola del Stage 1 booteado: kill $(pidof hammerd) → arje-zero lo
supervisa de verdad:
  Ente disuelto      label=hammerd  status=Killed(SIGTERM)
  Restart programado label=hammerd  delay_ms=200
  Ente encarnado     label=hammerd  pid=Some(Pid(64))

Detecta la muerte con su ExitStatus, hace backoff y re-encarna hammerd (pid
59→64). Es la supervisión real del init propio (ADR 0007) que la Fase 5 había
diferido. Runbook §8 y roadmap actualizados. Pendiente: B.2 (exponerlo a la
capa de IA por agent.sock) y atestación A1/A2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:41:35 +00:00
SergioandClaude Opus 4.8 186ae3e1c8 docs: Stage 1 bootea end-to-end en QEMU (bitácora + roadmap)
La corrida real cierra el lazo: con el build nativo crt-static (cargo rustc),
arje-zero y hammerd salen statically-linked (ET_EXEC, sin DT_NEEDED), el rootfs
se sella, y el boot en QEMU (-cpu Broadwell) levanta el sistema:

  arje-zero (PID 1) → carga+valida /ente/seed.card.json → bucle primordial →
  encarna hammerd (Pid 59) + console-getty (Pid 60) → shell en consola;
  hammerd levanta su watcher fanotify y el agent.sock.

Runbook §8: bitácora completa (todos los fixes: gcc/HOSTCC, GNU-ld, rust en el
lab, triple nativo, wrapper saneador, vendoring condicional, AVX→Broadwell,
dinámico→crt-static vía cargo rustc). Roadmap: Stage 1 ◑→.

Pendiente: demo del CRASHED real (kill hammerd → restart con backoff; la
supervisión Restart ya corre), bus único (B.2), atestación (A1/A2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:39:28 +00:00
SergioandClaude Opus 4.8 879af17f5c build: flags estáticos vía cargo rustc -- … (no RUSTFLAGS) para no romper proc-macros
Tercera iteración del fix de la corrida real (runbook §8). +crt-static (y
relocation-model=static) en RUSTFLAGS rompen los proc-macros en build nativo:
RUSTFLAGS alcanza a las deps, y un proc-macro es un dylib PIC que no puede ser
estático ("cannot produce proc-macro for clap_derive ... crate types").

Fix: pasar los flags de codegen del binario por `cargo rustc --release … -- <flags>`,
que los aplica SÓLO a la crate top-level, dejando proc-macros/deps intactos. Así el
binario final es crt-static + relocation-model=static (AUTOCONTENIDO y ET_EXEC sin
interpreter, como busybox: sin libc.so, sin loader, sin soname). El linker (zig cc)
sigue por RUSTFLAGS. musl queda --disable-shared (no se necesita loader).

Validado por unit tests; la corrida real rebuildea hammerd/arje-zero crt-static.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:24:01 +00:00
SergioandClaude Opus 4.8 ce7b570a62 build: recetas Cargo estáticas = crt-static + no-PIE (binarios autocontenidos)
Cierra el último bloqueo del boot que la corrida en QEMU destapó (runbook §8):
arje-zero/hammerd buildeaban DINÁMICOS contra musl (el rust de Alpine es dinámico
por defecto) → DT_NEEDED libc.musl-x86_64.so.1, y nuestro libc.so (musl shared con
zig cc) no exporta memcpy/memset/… → el loader falla y el init muere.

Fix golden-path: para link=static, RUSTFLAGS ahora fuerza
  -C target-feature=+crt-static -C relocation-model=static
⇒ binarios AUTOCONTENIDOS (musl dentro), ET_EXEC sin interpreter, como busybox:
sin libc.so, sin loader, sin el soname de Alpine. musl vuelve a --disable-shared
(no se necesita el loader). Boot en QEMU: usar -cpu Broadwell (el qemu64 default no
tiene el AVX que zig emite).

Validado por la corrida real: los 4 componentes buildan y el rootfs se sella; el
kernel arranca el initramfs y ejecuta arje-zero como PID 1. Falta rebuildear
hammerd/arje-zero con crt-static para cerrar el boot (cache-bust + rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:45:11 +00:00
SergioandClaude Opus 4.8 8d98a6053a build: vendoring con --locked condicional al Cargo.lock committeado (Opción 2)
Desbloquea recetas Cargo cuya fuente no committea Cargo.lock (p.ej. el monorepo
tawasuyu, que lo gitignora) sin tocar la política de ese repo.

vendor_cargo_deps: si la fuente trae Cargo.lock → `cargo vendor --locked`
(reproducible, deps pineadas por el lock del commit). Si no → vendoreo sin
--locked (genera el lock al vuelo) + warn! explícito de "build no pineado en el
tiempo". El end-state reproducible sigue siendo committear el lock (plan C.2 #5).

+1 test del camino sin-lock. Aplica a arje-zero (tawasuyu); hammerd (hammer,
que sí committea el lock) sigue por el camino --locked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 06:21:08 +00:00
SergioandClaude Opus 4.8 4895c1c884 build: BuildSys::Cargo nativo para el target del sandbox (Opción A) — hammerd builda
Resuelve el bloqueo Rust de la primera corrida: el rust de Alpine es
x86_64-alpine-linux-musl y --target x86_64-unknown-linux-musl no tiene std.

Cuando recipe.build.target == SANDBOX_NATIVE_TARGET (x86_64-linux-musl, el del
sandbox), se construye NATIVO (sin --target): el std nativo del rust del sandbox
sirve. El wrapper .hammer-zig-cc ahora:
- es un único ejecutable (resuelve el `zig cc` de dos palabras que cargo y cc-rs
  mal-parsean),
- SANEA el triple: cc-rs (build scripts, p.ej. blake3) detecta el host triple de
  Alpine y pasa --target=x86_64-alpine-linux-musl, que zig rechaza
  (UnknownOperatingSystem); lo reescribe a x86_64-linux-musl.
Se usa como CC (cc-rs) y como linker (RUSTFLAGS). El cross real conserva --target.

Validado en VM real: hammerd compila nativo con zig cc y se sella. +1 test (cross
mantiene el triple), detect_cargo actualizado. arje-zero queda bloqueado aparte:
tawasuyu no commitea Cargo.lock (vendor --locked falla) — ver runbook §8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:51:05 +00:00
SergioandClaude Opus 4.8 5a1088d5ce bootstrap: fixes de la primera corrida real de Stage 1 (musl+busybox ✓ con zig cc)
Primera ejecución del runbook en el host. El userland C builda end-to-end; el
camino Rust queda bloqueado por una decisión de toolchain. Hallazgos verificados:

recipes/busybox.toml — tres incompatibilidades GNU-toolchain ↔ zig, resueltas:
- gcc hardcodeado (CC/HOSTCC = gcc con `=` ignora el env): pasar CC='zig cc'
  HOSTCC='zig cc' en línea de comando de make
- flags GNU-ld que zig cc valida y rechaza (--warn-common, -Map, --verbose):
  quitarlos de scripts/trylink con sed
Con esto musl 1.2.5 y busybox 1.36.1 compilan, linkean y se sellan.

scripts/bootstrap-devfs.sh — añade rust+cargo al rootfs (las recetas Cargo
corren cargo en el sandbox), con CAVEAT: el rust de Alpine es
x86_64-alpine-linux-musl pero BuildSys::Cargo pide x86_64-unknown-linux-musl
(no instalado, sin rustup) → bloquea hammerd/arje-zero. Decisión de toolchain
abierta (plan C.2).

docs/runbooks/stage1-vm-boot.md §8 — bitácora con la tabla de etapas, los fixes
y las opciones para resolver el triple Rust.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:21:18 +00:00
SergioandClaude Opus 4.8 56af1b5748 docs: runbook para validar Stage 1 booteando en QEMU
Procedimiento operativo end-to-end para de-riesgar lo construido: stage0 (ingerir
zig) → stage1 (build + ensamblar el rootfs) → empaquetar initramfs (cpio newc +
gzip) → bootear en QEMU con el kernel del host → criterios de éxito.

Fundamentado en los comandos reales del repo (scripts/bootstrap-devfs.sh, la URL
y sha256 de zig 0.16.0, hammer bootstrap stage0/stage1). La prueba clave: matar
hammerd y ver a arje-zero reiniciarlo con backoff — el CRASHED real.

Honesto sobre su estado: es el procedimiento, no un transcript verificado; el
cross-compile y el boot reales aún no se corrieron (ese es el punto). §7
anticipa los ajustes probables (init, /dev/console, getty/tty, -lgcc_s).

Enlazado desde SDD 12 §I2 y el índice de docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 02:00:29 +00:00
SergioandClaude Opus 4.8 14e10d9292 bootstrap: arje-zero como PID 1 de Stage 1 (I2 del SDD 12)
Reemplaza el init provisional de busybox por arje-zero como PID 1 — el paso
"init real" que entrega el CRASHED real (Fase 5 diferida, ADR 0007).

- STAGE1_COMPONENTS += arje-zero (la receta puente ya lo construía)
- assemble_rootfs: genera /ente/seed.card.json (template autocontenido), crea
  /sbin/init -> /usr/bin/arje-zero, y los mount points que arje monta (incl.
  /ente, /var/lib/hammer, /sys/fs/cgroup, /dev/pts, /dev/shm). Se retira el inittab.
- la seed declara hammerd como Payload::Native Restart (su on_death = el CRASHED
  real) + console-getty supervisada; ULIDs fijos ⇒ RootfsHash reproducible (v2)
- la seed se VERIFICÓ contra el tipo real card_core::Card (from_json + validate
  pasan), no sólo como JSON — evidencia, no aserción
- +2 tests (ensamblado con arje init + seed válida); 21 verdes en el crate

Falta para cerrar Stage 1: bus único (B.2) y atestación (A1/A2). Boot real en VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:55:52 +00:00
SergioandClaude Opus 4.8 4a1d30db92 docs: SDD 12 — arje como init real del Stage 1 (diseño del contrato de runtime)
Diseña el último paso ◑ del Stage 1: reemplazar el init provisional de busybox
por arje-zero como PID 1, lo que entrega el CRASHED real (Fase 5 diferida).

Fundamentado en el código real de arje-zero (citas a tawasuyu):
- contrato de boot: arje monta los pseudo-FS él mismo (arje-kernel); lee
  /ente/seed.card.json (Card Virtual + genesis); supervisa con Restart/Backoff
- qué debe proveer el rootfs (mount points, seed, /sbin/init→arje-zero, consola)
- la seed card de Stage 1: hammerd como Payload::Native Restart ⇒ on_death = el
  CRASHED real; console-getty supervisada; ENTE_BUS_SOCK estable en /run
- generación de la seed: template JSON autocontenido v1 (no acoplar build a
  tawasuyu), arje-packager al llegar la atestación A1
- fases I1()→I2(PID 1)→I3(bus único B.2)→I4(overlay+atestación)
- relación agent.sock (API IA, encima) vs arje-bus (control del init)

Contraparte en hammer del plan tawasuyu PLAN-ATESTACION-Y-HAMMER §B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:48:01 +00:00
SergioandClaude Opus 4.8 778c3900ea recipes: arje-zero como receta puente Cargo a tawasuyu (track arje, build-side)
Puente concreto hacia "arje es el init de hammer" (ADR 0007): recipes/arje-zero.toml
construye el init PID 1 de arje con el lab de hammer.

- Cargo recipe: fuente = monorepo tawasuyu a commit fijado (el de la migración A0
  arje-cas → BLAKE3, así el CAS es coherente con hammer), -p arje-zero, estático
  con zig cc; deps vendoreadas en el fetch (Lote 6) ⇒ build --offline
- prueba que el lab cruza al repo de tawasuyu y produce el binario del init; NO es
  PID 1 todavía (eso es el paso "init real": seed card + arje-bus + hammerd Card)
- nuevo test que escanea recipes/ y valida que toda receta parsea + hashea offline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:39:56 +00:00
SergioandClaude Opus 4.8 02827f62c9 bootstrap: hammerd como receta Cargo en Stage 1 (Lote 7)
Cierra el userland mínimo salvo el init definitivo. hammerd entra como tercer
componente de stage1, construido por el lab vía BuildSys::Cargo + vendoring:

- recipes/hammerd.toml: source git del repo hammer a commit fijado (ADR 0006),
  flags=["-p","hammerd"] para construir sólo ese binario del workspace, estático
  con zig cc; las deps se vendorean en el fetch (Lote 6) ⇒ build --offline
- STAGE1_COMPONENTS += hammerd; el inittab provisional lo arranca como servicio
  respawn (/usr/bin/hammerd) tras montar los pseudo-FS
- el test de recetas del repo ahora valida también hammerd.toml (parse + hash)

Falta sólo sustituir el init provisional de busybox por arje (ADR 0007) para el
CRASHED real. El cross-compile se valida en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:06:40 +00:00
SergioandClaude Opus 4.8 e9b0551213 build: vendoring de deps Cargo en el fetch para builds --offline (Lote 6)
Completa BuildSys::Cargo: las recetas Rust necesitan sus deps de crates.io
disponibles para el `cargo build --offline` hermético del sandbox. fetch::
vendor_cargo_deps corre `cargo vendor --locked` (red OK fuera del sandbox) y
escribe .cargo/config.toml en el árbol del source; build() lo invoca cuando
detecta una receta Cargo, tras los patches.

- --locked fija por Cargo.lock (dentro del commit que hashea la receta) ⇒ sin
  no-determinismo nuevo: mismo commit ⇒ mismas deps vendoreadas
- crate sin deps ⇒ config vacío, inocuo
- test gated (skip si no hay cargo) del camino spawn → stdout → config.toml

Habilita hammerd (y a futuro arje) como recetas Cargo construibles offline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 01:04:46 +00:00
SergioandClaude Opus 4.8 60a9451a03 bootstrap: stage1() ensambla el userland mínimo (Lote 5)
Cross-compila musl + busybox con la semilla de Stage 0 (no el zig del host),
ensambla un rootfs FHS por hardlink y lo sella. CLI: `hammer bootstrap stage1`.

- seed_toolchain_dir(store, hash, kind): primitivo sin SeedSpec, para que Stage 1
  resuelva el toolchain desde (seed_hash, kind) sin re-pasar url/sha256
- RootfsHash = hash de CONTENIDO (componentes ordenados + init), no del árbol en
  disco ⇒ reproducible aunque difieran inodes/timestamps
- assemble_rootfs: esqueleto FHS + hydrate de cada componente + /etc/inittab
  provisional (busybox init); descarta el sidecar .hammer del rootfs
- idempotente (no reensambla si el rootfs ya está sellado); anota línea stage 1
- hammerd (receta Cargo) y arje quedan como slots pendientes (plan M2 / ADR 0007)

+3 tests (rootfs_hash determinista/sensible, assemble_rootfs, recetas del repo
parsean y hashean). Workspace verde; el cross-compile real se valida en la VM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:43:03 +00:00
Sergio c5ab7e4502 feat(build): BuildSys::Cargo — el lab construye crates Rust
Desbloquea integrar el workspace tawasuyu (y cualquier crate Rust) en el lab:
- detección por Cargo.toml (gana sobre Makefile huérfano, cede ante CMake de
  un políglota C+Rust).
- traducción de triple zig→rustc (x86_64-linux-musl → x86_64-unknown-linux-musl).
- compile: cargo build --release --locked --offline --target <triple>; link por
  wrapper zig cc (CARGO_TARGET_<T>_LINKER) que provee el libgcc_s que el link
  dinámico musl exige (cierra el 'cannot find -lgcc_s' que destapó M2). link=dynamic
  añade -C target-feature=-crt-static para dlopen (apps gráficas: Vulkan/Wayland).
- install: copia los ejecutables de target/<triple>/release a /out/usr/bin
  (busybox-safe).
- doc 02-build-lab §Cargo (vendoreo de deps para build offline) + receta de ejemplo
  recipes/llimphi-counter.toml (caso gráfico dinámico).
- 5 tests nuevos (34/34 verdes en hammer-build).

Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER §C (milestone:
falta BuildSys::Cargo en el lab).
2026-06-11 00:37:33 +00:00
SergioandClaude Opus 4.8 4221f84ac7 recipes: musl 1.2.5 + busybox 1.36.1 pinned (Lote 4, base C del Stage 1)
Las dos piezas C del userland mínimo (SDD 11 §3), estáticas con zig cc cross
a x86_64-linux-musl, sha256 fijado (descargado y verificado, no inventado):

- musl 1.2.5   a9a118bbe84d...  (libc del target: headers + libc.a)
- busybox 1.36.1 b8cc24c9574d... (coreutils + sh + init provisional vía inittab)

busybox: defconfig + CONFIG_STATIC=y, TC desactivado (gcc-ismos incompatibles
con musl); install por CONFIG_PREFIX crea los symlinks de applets para el rootfs.

hammerd queda fuera por ahora: con el soporte Cargo del lab ya es construible,
pero su sourcing (repo pinned + vendor para --offline) es parte del plan M2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:36:39 +00:00
SergioandClaude Opus 4.8 989e0d2c7f bootstrap: seed como toolchain del sandbox (Lote 3) + decisiones ADR 0008
SeedSpec::toolchain_dir resuelve el dir del compilador dentro de la semilla
sellada, listo para que el sandbox lo bindee como /opt/zig; seed_build_config
deriva una BuildConfig que cross-compila con el seed pinned, no con el zig del
host. Stage 0 sigue siendo ingesta pura: el layout se resuelve al usarlo
(zig en la raíz o en el hijo versionado), con error claro si falta o es ambiguo.

Decisiones del Lote 4 registradas en ADR 0008 / SDD 11:
- coreutils+shell = busybox (balsa desechable; GPLv2 no contamina el final)
- init arje diferido (Stage 1 con init mínimo provisional; CRASHED real luego)
- layout de semilla: ingesta pura + resolución al usar

+5 tests (layouts: raíz / hijo versionado / no-sellada / ambiguo / build_config).
Workspace verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:28:29 +00:00
SergioandClaude Opus 4.8 1f1fae3508 build: resolver deps.build recursivamente en artifact_hash (cierra TODO fase-0)
Lote 2 del track posterior. El hash de un artefacto ahora incluye el hash de
cada dep de build (Merkle sobre el subgrafo), no solo su fuente+flags.

- catálogo = base_dir de la receta: cada dep se resuelve a <base_dir>/<dep>.toml
  (firma de artifact_hash/build sin cambios; cero ripple en call sites)
- detección de ciclos con la cadena en el mensaje (a -> b -> a)
- errores claros: dep .toml ausente, o deps sin base_dir (receta de TOML crudo)
- 6 tests nuevos; ninguna receta actual declara deps ⇒ sin impacto en e2e

Prepara Stage 1: seed_hash entrará al hash de toda receta del userland mínimo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:59:34 +00:00
SergioandClaude Opus 4.8 ba4d520c44 Bootstrap: manifiesto versionado (bootstrap.json) + Stage 0 anota su línea
Lote 1 del track posterior. Implementa el embrión del log de transparencia
(SDD 11 §4): crate hammer-bootstrap::manifest con BootstrapManifest/StageEntry.

- append-only e idempotente (de-dup por (stage, artifact_hash))
- escritura atómica (tmp + rename); ts informativo, fuera de todo hash
- stage0() anota su línea en ambos caminos (sello nuevo e idempotente);
  un sha erróneo no genera línea
- 9 tests nuevos (4 de manifest + 5 de stage0/manifest), workspace verde

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:54:28 +00:00
SergioandClaude Opus 4.8 1731e7ef23 CLI: hammer bootstrap stage0 + limpieza de staging y e2e
Cablea el Stage 0 del bootstrap a la CLI: subcomando `bootstrap stage0
--url --sha256 --version [--seed zig|musl-cross-make]` que abre el store,
arma el SeedSpec y llama a hammer_bootstrap::stage0, imprimiendo el b3 del
artefacto sellado.

- stage0 ahora limpia el staging `.bootstrap-tmp` pase lo que pase (extraído
  stage0_into): un sha256 erróneo ya no deja el tarball a medio descargar bajo
  el store. Test reforzado para afirmarlo.
- e2e de CLL `bootstrap_cli.rs`: tarball falso vía file:// → sella + imprime
  hash, idempotente, sha erróneo falla sin sellar. Sin red.

Docs SDD 11 §5 y roadmap marcan Stage 0 (lib+CLI) hecho; Stage 1/2 pendientes.
Workspace 219 verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:22:44 +00:00
SergioandClaude Opus 4.8 3d6d25661c Track posterior: Stage 0 del bootstrap (crate hammer-bootstrap) + ADR 0008
Primer eslabón ejecutable del bootstrap from-scratch (SDD 11). Stage 0 ingiere un
toolchain semilla pinned al store sin compilarlo:

- SeedSpec{kind,version,url,sha256}. seed_hash() deriva el ArtifactHash de la
  identidad pinned (clase+versión+sha256), no de rutas del host ni del url → el
  mismo tarball desde otro espejo produce el mismo artefacto.
- stage0(seed, store): descarga (curl, file:// offline-ok), verifica sha256
  ANTES de extraer, untar y seal en el store. Idempotente si ya está sellado;
  un sha erróneo no sella nada.
- Reusa el lab: hammer_build::download (+ nuevo verify_sha256 público), Store::seal.
  Staging bajo el root del store para que el rename de seal sea atómico.

ADR 0008 fija la decisión (3 stages, zig semilla primaria, musl-cross-make
escotilla, ingerir-no-compilar el Stage 0). Renumerado a 0008 porque el 0007 lo
tomó la decisión de adoptar arje como init; SDD 11 y refs alineados con arje
(init supervisado que entrega el CRASHED real, ADR 0007).

4 unit tests (hash por identidad, ingest+seal+readonly, idempotencia, rechazo de
sha) sin red (file://). Workspace 218 verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:03:09 +00:00
Sergio ea10c9f716 docs(adr): 0007 — adoptar arje como init del track posterior
Coordina con tawasuyu/03_ukupacha/arje/PLAN-ATESTACION-Y-HAMMER.md:
- arje (init from-scratch ya existente, PID1 + supervisión real) cubre el
  CRASHED diferido de la Fase 5 en vez de escribir init nuevo.
- frontera: PID1 fino, un solo bus (arje-bus + hammer-core::proto), CAS
  unificado en BLAKE3, confianza en capas (procedencia hammer + atestación
  arje; el expected_hash del .swm = el blake3 que arje atesta).
- caveat: la cage glibc es del mundo hammer/Linux, fuera de PID1.
Puntero añadido en docs/10-roadmap.md §Track posterior.
2026-06-10 20:57:17 +00:00
SergioandClaude Opus 4.8 bbab5a204d docs: SDD 11 — bootstrap from-scratch (abre el track posterior)
Diseña el corte del cordón umbilical con Alpine en tres etapas, reusando el lab
de Fase 0 sin mecanismo nuevo:

- Stage 0: toolchain semilla (zig primario, musl-cross-make como escotilla)
  ingerido al store como fuente pinned por sha256 — el único insumo del host.
- Stage 1: userland mínimo cross-compilado (musl, busybox/toybox, init propio,
  hammerd) ensamblado en un rootfs sellado. El init propio habilita el CRASHED
  real diferido de Fase 5.
- Stage 2: rebuild nativo dentro del rootfs y diff de hashes stage1 vs stage1'
  ⇒ auto-alojamiento bit-reproducible.

Cada etapa anota (stage, recipe_hash, artifact_hash) en bootstrap.json, embrión
del log de transparencia (SDD 09 §4). Propone el crate hammer-bootstrap y la CLI
`hammer bootstrap stage0|stage1|stage2|--all`. Decisiones abiertas marcadas para
un futuro ADR 0007. Enlazado desde el índice de docs y el roadmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:51:13 +00:00
SergioandClaude Opus 4.8 9f7c1aa040 docs(roadmap): Fases 3/4/6 → , Fase 5 con CRASHED diferido, estado actual al día
Sincroniza las cabeceras con la realidad: tras cerrar la op del watcher (Fase 3)
y la anidación LIFO (Fase 2), ningún ítem de las fases 0–6 queda abierto salvo
CRASHED real, explícitamente diferido al track posterior. Reescribe "Estado
actual" (seguía describiendo el día 1) con el bucle agéntico validado, 214 tests
verdes y el siguiente paso: bootstrap from-scratch del track posterior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:41:56 +00:00
SergioandClaude Opus 4.8 fe9a2d50cf Fase 2: anidación real de overlays con guard LIFO
Un `try` sobre un target ya cubierto apila un overlay nuevo: el kernel toma el
merged view de la capa inferior como lowerdir. Para que esto sea seguro y no un
footgun:

- fresh_id añade un `seq` atómico por proceso (`<ts>-<pid>-<seq>` zero-padded),
  eliminando la colisión de ids cuando un orquestador apila varios overlays en
  el mismo segundo — el caso real de la anidación.
- stack_key define un orden de apilamiento total y determinista (created_at,
  desempatado por id).
- blocking_overlays detecta capas más jóvenes que solapan targets (igualdad o
  ancestro de path). commit/discard fallan con Error::Shadowed si existen,
  exigiendo resolver LIFO de arriba hacia abajo — antes se desmontaba la capa
  equivocada del target compartido en silencio.

Tests: 6 unit del guard (disjuntos / solape / ancestro / desempate / commit y
discard rechazados) sin privilegios; e2e overlay_nested_stack_inside_userns
prueba el stack real (capa 2 ve la 1, rechazo LIFO, fusión arriba→abajo),
gated en HAMMER_OVERLAY_TESTS. Docs 04 y roadmap actualizados.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:39:32 +00:00
SergioandClaude Opus 4.8 303dfa2b58 Fase 3: op del watcher (Create/Edit/Delete) derivada del diario
CLOSE_WRITE ya no se materializa siempre como Replace. classify_op deriva:
- Delete: el fd resuelve a " (deleted)" (sufijo que ahora recortamos del path
  en vez de colarlo al diario; content_hash queda None).
- Edit: el diario ya tenía un evento previo no-Delete para el path.
- Create: primera mutación observada, o resurrección tras un Delete.

Es la divergencia que el diario atestigua, no la verdad absoluta del FS. La
atribución plena vía FAN_REPORT_DFID_NAME queda diferida: nix 0.30 no parsea
los info-records de FID y el modo FID perdería el fd que hashea el contenido.

classify_op se extrae como función libre para probarla sin CAP_SYS_ADMIN; 4
tests unitarios nuevos. Roadmap Fase 3 actualizado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:33:00 +00:00
SergioandClaude Opus 4.8 6c34484fbe Fase 3: content_hash post-mutación + de-dup idempotente en el diario
El campo content_hash (ya existía en MutationEvent) ahora se puebla y se
usa para no ensuciar el diario con reescrituras sin cambios.

hammer-journal:
- content_hash_of(bytes) / hash_file(path): blake3 plano (estilo b3sum,
  "b3:<hex>"), distinto del of_inputs de artefactos — aquí la pregunta es
  "¿cambió el archivo?".
- last_for_path(path): último evento de un path.
- record_dedup(ev): omite el evento si deja el archivo idéntico al último
  estado de ese path (content_hash igual) o si es un Delete sobre algo ya
  borrado. Devuelve si escribió o no. +7 tests.

hammerd watcher: hashea cada CLOSE_WRITE y usa record_dedup; una
reescritura idéntica ni registra ni emite Modified al bus.

22 binarios de test verdes, sin warnings nuevos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 20:28:16 +00:00
SergioandClaude Opus 4.8 7a6bfb4c4d Fase 4: patch_url / content_url remotos en .swm
Cierra el último hueco de "sólo inline": ahora un .swm puede referenciar
el patch y el contenido de un file_drop por URL.

- hammer-build/src/download.rs: fetch_url_bytes / fetch_url_to_file (curl,
  sólo fuera del sandbox; acepta file:// para tests offline). 3 tests.
- swm_bridge::build_source_patch: descarga patch_url a
  swm-recipes/<commit>.patch antes de compilar (ya no lo rechaza). Test
  reescrito con file://.
- CLI apply: file_drop con content_url descarga y verifica BLAKE3 ANTES de
  escribir; hash erróneo ⇒ nada tocado (verificado e2e).

Integridad: content_hash (file_drop) y build reproducible + expected_hash
(patch). 22 binarios de test verdes; e2e por file:// (apply + caso de
hash inválido que no escribe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:57:05 +00:00
SergioandClaude Opus 4.8 6db3e3e302 Fase 5: BuildFailed.log_tail con la cola real del log del sandbox
Cuando una fase de build falla, el lab ahora conserva las últimas 50
líneas del log y las entrega a la IA por el bus.

- sandbox.rs: BuildFailure { reason, log_tail } (impl Error), wrap en
  Error::Other. Sandbox::run hace tee de stdout/stderr al padre (streaming
  en vivo intacto) y a un ring buffer acotado; al fallar, devuelve la cola
  en log_tail. spawn_pump + push_tail con tests.
- BuildFailure::from_error(&Error) recupera la struct por downcast.
- bus.rs run_compile: separa reason del log_tail real del compilador en
  vez de mandar e.to_string() y log_tail=None.

Tests: push_tail (ring), roundtrip por downcast, none para errores planos.
Firmas de build/build_source_patch sin cambios. 22 binarios verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:41:51 +00:00
SergioandClaude Opus 4.8 42288230f9 Fase 5: política de caps del bus desde agent-caps.toml
Sustituye la política hardcodeada por una declarativa (SDD 07 §4).

hammer-core/src/caps.rs:
- AgentCapsConfig { default, rule[] }; CapRule { uid?, gid?, caps }.
- caps_for(uid, gid): primera regla que casa (todos los campos
  declarados deben coincidir) o default. load(path) → Ok(None) si falta.
- 8 tests: orden de reglas, match uid+gid, fallback, regla vacía ignorada.

hammerd:
- bus::policy_from_config(cfg) construye la CapsPolicy desde la config.
- main: --agent-caps (default /etc/hammer/agent-caps.toml). Con fichero
  usa la config; sin fichero o con fichero inválido cae a default_policy
  (avisando). +1 test del policy_from_config.

examples/agent-caps.toml: plantilla comentada.

22 binarios de test verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:32:38 +00:00
SergioandClaude Opus 4.8 5ec7a81e87 Fase 4: firma Ed25519 del .swm + TrustStore local
Cierra el item de firma del modelo de confianza (SDD 09 §3, §5).

hammer-core/src/sign.rs:
- KeyPair: genera (getrandom), carga/serializa claves base64, escribe
  <name>.ed25519 (0600) + <name>.ed25519.pub, y firma un Swm.
- TrustStore::load(dir): lee *.ed25519.pub; dir ausente ⇒ store vacío.
- Swm::verify_signature(&trust) → SigStatus {Trusted|UnknownKey|BadSig|
  Unsigned}. Bytes firmados = JSON canónico de (swm_version, base,
  mutations) sin la firma; deterministas (pins en BTreeMap).
- 8 tests: sign/verify, tamper, wrong-key, roundtrip en disco, dir ausente.

CLI:
- hammer keygen <name> [--out DIR]
- hammer swm-sign <file> --key K [--by N] [-o OUT]
- hammer swm-verify <file> --trust DIR  (reporta trusted/unknown-key/
  bad-sig/unsigned; bad-sig aborta, lo demás informa).

La firma da autoría + integridad pero NO autoriza promover: apply sigue
reproduciendo y comparando. Verificado e2e por el CLI (keygen→sign→verify
en los 4 estados). 22 binarios de test verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 19:28:32 +00:00
Sergio de036d3666 Fase 4 — provenance en hammer export (sidecar de receta en el store)
Hasta ahora `hammer export` emitía sólo `file_drop` con `content_b64`.
Eso reproducía byte-a-byte pero perdía la receta: el receptor obtenía
binarios opacos, sin manera de auditarlos ni de recompilarlos desde
fuente. Esta fase cierra el hueco para los artefactos producidos por
`hammer-build::build`.

Mecanismo:
- `Store` reserva `.hammer/recipe.toml` (`RECIPE_SIDECAR_REL`) dentro
  de cada artefacto. `hammer-build::build` lo escribe antes de sellar,
  así queda inmutable junto con el árbol.
- `Store::recipe_for_dir` / `recipe_for_hash` lo leen de vuelta. Si el
  artefacto es viejo y no trae sidecar, devuelven `None` sin fallar.
- `Recipe::to_toml` (nuevo) hace el roundtrip.

Lógica del export (función pura `build_export_mutations`):
1. Aplica semánticas de Delete: invalida estados previos del mismo path.
2. Particiona los eventos sobrevivientes en (a) los que tienen
   `artifact_hash` con receta sidecar y (b) el resto.
3. Por cada grupo de (a) emite UN `source_patch` con repo+commit+build
   de la receta y `expected_hash = artifact_hash`. El `target_bin` es
   el primer path alfabético del grupo; el receptor hidrata el árbol
   completo al aplicar.
4. Patches de la receta se concatenan inline en el `source_patch.patch`.
5. Los eventos del bucket (b) caen al fallback `file_drop` con
   `content_b64 + content_hash` (orden alfabético).

Limitaciones explícitas:
- `SourcePatch` sólo modela `Source::Git`. Recetas con `tarball` se
  reportan por stderr y caen a file_drop. Extender el SWM para
  tarballs es trabajo aparte.
- Si la receta tiene patches pero alguno no se puede leer, no se
  inlina; `expected_hash` sigue siendo el gate de integridad para
  detectar la divergencia.

CLI: el subcomando `Export` ahora recibe `--store` para resolver
artefactos. Defaults igual que antes (`/store`).

Tests (12 nuevos):
- hammer-core (5): `Recipe::to_toml` roundtrip; `Store::recipe_for_dir`
  sin/con sidecar; `Store::recipe_for_hash` por prefijo + hash inexistente.
- hammer-cli (7): export sin store; semánticas de Delete; hydrate con
  sidecar emite source_patch agrupando dos archivos; hydrate sin sidecar
  cae a file_drop contando `missing_recipe`; mix trazable + external;
  path ilegible sólo warning.
2026-06-10 16:49:09 +00:00
Sergio 5986200539 Fase 6 — traductor LLM real (Claude API) detrás de feature llm-claude
`ClaudeTranslator` implementa el trait `IntentTranslator` igual que el
`MockTranslator`, así que el `Orchestrator` no cambia: se traduce una
intención NL a un `.swm` válido vía `/v1/messages`.

Diseño:
- Síncrono (ureq + rustls), consistente con `AgentClient`. Tokio
  entraría sólo si el resto del crate lo pidiera.
- Trait `HttpClient` inyectable ⇒ tests sin red contra una fake que
  captura el request y devuelve un body pre-armado.
- Modelo por defecto: `claude-opus-4-8` (Opus 4.8, el más capaz al día
  de hoy). Adaptive thinking + `effort=high` por defecto. Override por
  env (`HAMMER_LLM_MODEL`, `HAMMER_LLM_EFFORT`, `HAMMER_LLM_BASE_URL`).
- System prompt documenta el shape exacto del `.swm` (4 variantes de
  mutación) y obliga JSON puro. Parseamos con `serde_json::from_str::
  <Swm>` + `verify_schema()` como gate adicional. Manejamos `refusal`,
  error envelopes de la API y code-fence markdown.

Activación:
- Sin feature: el módulo declara los tipos pero `ClaudeTranslator::new`
  está bajo cfg. El binario compila sin red.
- Con `--features llm-claude`: trae `ureq` con rustls, y la CLI activa
  `hammer ai --llm`.

CLI:
- `hammer ai` gana `--llm` (mutuamente excluyente con `--catalog`).
  Refactor de `run_ai` en helpers `run_with_mock_translator` /
  `run_with_llm_translator` para mantener legible el dispatch.
- Sin la feature, `--llm` falla con un mensaje claro pidiendo
  recompilar.

Tests (7 unit): happy path con verificación de URL/headers/body,
unwrap de markdown, `refusal`, error envelope, SWM mal formado,
verify_schema, helpers de strip_code_fence.
2026-06-10 16:29:27 +00:00
Sergio f769d9173c Fase 6 — bucle de auto-reparación (cliente reacciona a Crashed)
`Orchestrator::run_with_repair(intent, &policy)` corre el ciclo
plan→try→apply→wait_for_crash→re-plan hasta `max_attempts` o
estabilización (sin Crashed en `crash_window`). Cuando llega un Crashed
asíncrono por el bus, la política convierte `(service, code)` en una
nueva intención (`default_crash_intent` por defecto) y reentra.

Diseño:
- `RepairPolicy { max_attempts, crash_window, event_sock, format_intent }`
  acota el blast-radius desde el caller; la IA no decide reintentar.
- `Proposal` gana `repair_chain: Vec<RepairAttempt>` con la cadena de
  intentos (intent, overlay_id, triggered_crash). La IA NUNCA promueve
  al FHS; el humano lee el chain y decide commit/discard.
- `wait_for_crash` abre un AgentClient sólo para drenar async events;
  reusa toda la maquinaria síncrona del cliente.

CLI: `hammer ai --repair-max-attempts N --repair-window-ms M`. Sin el
flag, `run` corre una sola vez (compatible hacia atrás).

Tests (3): stub bus multi-conexión que pre-programa eventos por
conexión. Cubre reacción a Crashed, cap por max_attempts con crashes
persistentes, y modo sin socket (una sola corrida).
2026-06-10 16:27:59 +00:00
Sergio dbbc10e854 Fase 6 — lenguaje de consulta del sistema (SDD 08 §6)
Forma `kind:value`: `bin`, `file`, `pin`, `service`, `depends`. El
evaluador acepta un EvalContext con BaseRef, fs_root y path_env para
re-rootear contra un overlay o prefix sin tocar el FHS real. `depends:`
implementa un parser ELF64 LE mínimo que recorre PT_DYNAMIC para extraer
DT_NEEDED, suficiente para los binarios estáticos+dinámicos del lab.

- hammer-core::query: parse(expr) + eval(term, ctx) + eval_str(expr, ctx).
- proto: Command::Query gana `expr: Option<String>`.
- AgentClient::query_expr: equivalente a query_file pero por el bus.
- hammerd: maneja `what="expr"` invocando query::eval_str.
- CLI: `hammer query <expr> [--base-ref] [--fs-root]` evalúa en proceso.

Tests: 18 unit en `hammer-core::query::tests` (parse, eval con fs_root,
ELF gated en `HAMMER_HOST_ELF_TESTS`), 1 e2e en `hammerd::bus_e2e`
(query_expr_evaluates_against_host_path).
2026-06-10 16:25:49 +00:00
Sergio 89ecd54135 Fase 6 — bucle agéntico: hammer-agent (cliente + translator + orchestrator) y hammer ai
- proto: mover hammerd::proto a hammer-core::proto para que hammerd y hammer-agent
  compartan los tipos del bus sin duplicar.
- hammer-agent (crate nuevo):
  * client: AgentClient síncrono. Handshake hello/welcome; compile/inject/query/init
    bloqueantes con timeout; reader thread interno demultiplexa async events (Modified/
    Crashed) en una cola que el caller drena vía drain_async/next_async.
  * translator: trait IntentTranslator + MockTranslator (HashMap<intent, Swm>) +
    IntentCatalog YAML (swm_inline o swm_path). El traductor LLM real se enchufa
    detrás del mismo trait sin cambios al orquestador.
  * orchestrator: Orchestrator::run(intent) -> Proposal con plan -> schema -> base ->
    try (overlay|prefix) -> apply (config_edit/file_drop con hammer-core::apply,
    source_patch via bus opcional) -> verify (spot-checks) -> propose. Devuelve
    overlay_id (para `hammer commit`) o prefix usado.
- hammer-cli: subcomando `hammer ai <intent> --catalog F [--prefix DIR --base-ref F
  --bus SOCK --state-root DIR]`. Imprime el Proposal y el siguiente paso humano.
- Tests:
  * 10 unit (translator + catalog + orchestrator).
  * 3 e2e del bucle agéntico (intent -> archivos esperados bajo un prefix tmp).
  * 1 e2e del cliente contra un stub bus (handshake + Compile -> BuildReady +
    Modified asíncrono), sin depender de hammerd ni del lab.
- Docs: SDD 08 actualizado con el API del crate; roadmap marca lo cerrado y lo
  pendiente (LLM real, lenguaje de consulta, bucle de auto-reparación con Crashed).
2026-06-09 15:47:37 +00:00
Sergio f41022d81f Fase 5 — bus de agente (/run/agent.sock) + FIFO de control humano
- proto: tipos Command/Event con serde tag "t", Hello/Welcome handshake, RecipeInline
  reducida para COMPILE, Cap enum (Query/Compile/Inject/InjectReal/Init) y
  required_for() para gating por comando.
- bus: serve_agent_bus listens en UnixListener, autenticación SO_PEERCRED por
  conexión via nix::sys::socket::PeerCredentials, una conexión = reader thread +
  writer thread + bus-forwarder thread + worker spawn por COMPILE. Dispatch a
  hammer-build (Compile), find_by_hash+run_hydrate (Inject), stat (Query/file),
  store.find_by_hash (Query/artifact) y send_to_fifo (Init).
- events: EventBus in-process (Arc<Mutex<Vec<Sender>>>), suscripción por conexión
  y purga perezosa de subs muertos en publish.
- control: ensure_fifo (mkfifo idempotente, rechaza non-FIFO), run_reader (relog
  por línea, reabre al EOF), send_to_fifo (bloqueante si no hay reader).
- watcher: nuevo start_with_events que clona el EventBus; cada MutationEvent
  registrado se re-emite como Event::Modified a las conexiones abiertas.
- hammerd::main: orquesta FIFO + watcher + bus en threads; --no-watcher para
  dev sin CAP_SYS_ADMIN.
- hammer-cli: hammer ctl <line> [--fifo PATH] escribe al FIFO con error claro si
  no existe; reemplaza el stub "[fase 5 pendiente]".
- Tests:
  * 14 unit tests nuevos en hammerd (proto/bus/events/control).
  * 5 e2e en crates/hammerd/tests/bus_e2e.rs: handshake con peer creds, gating
    no_cap, Query/file, Modified fan-out vía bus, Init -> FIFO end-to-end.
- Docs: docs/10-roadmap.md actualizado con lo cerrado y lo pendiente (policy
  declarativa, CRASHED real con supervisor, log_tail en BuildFailed).
2026-06-09 15:29:42 +00:00
Sergio 09d50f9e60 Fase 4 — formato .swm: verify, apply (config_edit/file_drop/source_patch) y export
- hammer-core::swm: verify_schema (invariantes por mutación) + verify_base con
  BaseRef/BaseCompat/PinDiff (distro_version + pins). FileDrop.content_b64 para
  .swm autocontenidos.
- hammer-core::apply: primitivas puras apply_config_edit (hunks -/+ con búsqueda
  exacta de bloque, ambiguo => error), apply_file_drop (base64 + verify BLAKE3),
  rebase_path para tests con prefix.
- hammer-build::swm_bridge: Mutation::SourcePatch -> Recipe sintética + patch
  inline materializado + build, con comparación opcional contra expected_hash.
- hammer-cli: nuevos subcomandos apply [--prefix --base-ref --skip-source-patch
  --state-root], swm-verify, export --journal --base-ref --since.
- Tests: 18 unit nuevos en hammer-core (apply + verify), 5 en swm_bridge,
  4 e2e en hammer-cli (roundtrip yaml -> apply bajo prefix reproduce los archivos).
- Roadmap y SDD 06 actualizados con lo cerrado y lo pendiente (URLs remotos,
  provenance en export vía mapa artefacto->receta, firma ed25519).
2026-06-09 15:19:33 +00:00