# SDD 10 — Roadmap

## Estrategia: validar la capa AI-nativa sobre Alpine antes de la distro propia

La innovación de `hammer` no es la distro — es el substrato AI-nativo (build determinista +
mutable + diario + `.swm`). Construir bootstrap + init + userland de cero antes de validar esa
capa sería arriesgar meses contra una hipótesis no probada. Por eso:

> **Fase 0–6:** montamos `hammer` **sobre Alpine** (musl + busybox + FHS mutable, ya cocidos).
> Validamos el bucle completo en semanas.
> **Track posterior:** una vez probado, bajamos a distro propia (init propio, bootstrap propio,
> userland propio) reusando todo el tooling sin retrabajo.

Ver [ADR 0002](adr/0002-alpine-first.md).

---

## Fases (sobre Alpine)

### Fase 0 — Laboratorio de build  ✅
- [x] `hammer-core`: tipos `Recipe`, `ArtifactHash`, `Store` (BLAKE3, layout `/store`).
- [x] `hammer-build`: sandbox con `bubblewrap` + `zig cc` → binario musl estático.
- [x] CAS: hashing de entrada, caché por hash, sellado en el store.
- [x] `hammer build <recipe.toml>` → imprime el hash del artefacto.
- [x] Fuente git **y** tarball (sha256 fijado), patches opcionales.
- [x] Heurística de build system: autotools / cmake / meson / make plano.
- [x] Caché persistente de zig entre builds (~30s/build ahorrados).

### Fase 1 — Hidratación ✅
- [x] `hydrate(hash, target, mode)`: hardlink a FHS, `patchelf` para el caso dinámico.
- [x] `hammer hydrate <hash> --into /`.

### 🎯 Primer entregable (Fase 0 + 1) ✅
**GNU grep 3.12 compilado estático desde el tarball upstream, sellado en el store
(`b3:b81e893e…`), hidratado por hardlink a `/usr/bin/grep` y ejecutado dentro del rootfs
Alpine vía `bwrap`** — confirma el corazón "fábrica funcional → FHS mutable" sobre el
substrato Alpine. La VM/LXC dedicada queda como ejercicio de empaque, no como
pre-requisito de validación.

### Fase 2 — Overlay de experimentación  ✅
- [x] Crate `hammer-overlay`: `try` / `commit` / `discard` / `status` sobre `overlayfs`.
- [x] Manifiesto persistente en `<state_root>/<id>/state.json`; `status` enumera.
- [x] `commit` promociona archivos y procesa whiteouts (char dev 0/0) como remove.
- [x] Subcomandos del CLI: `hammer try [targets…]`, `commit <id>`, `discard <id>`, `status`.
- [x] Tests E2E con bwrap+user-ns, gateados en `HAMMER_OVERLAY_TESTS=1` (kernel-dependiente).
- [x] Hook al diario en `commit` (Fase 3 lo añadió vía `commit_with_journal`).
- [x] Anidación real de overlays. Un `try` sobre un target ya cubierto apila un overlay nuevo
      (el kernel usa el merged view inferior como `lowerdir`). Orden de apilamiento total y
      determinista vía `stack_key` (`created_at` + `id`, con `seq` por proceso en `fresh_id`
      para evitar colisiones en ráfaga). `commit`/`discard` exigen **LIFO**: `blocking_overlays`
      detecta capas más jóvenes que solapan targets (igualdad o ancestro de path) y la operación
      falla con `Error::Shadowed` si las hay. Guard cubierto por 6 unit tests; el stack real
      (capa 2 ve la 1, LIFO rechaza commit de abajo, commit arriba→abajo fusiona) por
      `overlay_nested_stack_inside_userns`, gated en `HAMMER_OVERLAY_TESTS`. Pendiente menor:
      unicidad de orden entre procesos concurrentes (track posterior).

### Fase 3 — Diario de mutaciones  ✅
- [x] Crate `hammer-journal`: `MutationEvent`, `Source` (HammerHydrate/HammerCommit/External),
      append/read/tail/follow JSON-líneas. RFC 3339 sin chrono.
- [x] `hammerd` con `fanotify` (FAN_CLOSE_WRITE) sobre `/bin`,`/sbin`,`/usr/bin`,`/usr/sbin`,
      `/lib`,`/usr/lib`,`/etc`. Filtra eventos bajo overlays activos. Fallback graceful sin
      CAP_SYS_ADMIN.
- [x] `hammer journal [--tail N] [--follow] [--format pretty|json]`.
- [x] `hammer commit` registra cada archivo promocionado vía `commit_with_journal`.
- [x] Refinar la `op` del watcher. `watcher::classify_op(&journal, path, deleted)` deriva
      `Delete` (el fd resuelve a `" (deleted)"`, que ahora recortamos del path en vez de
      colarlo al diario), `Edit` (el diario ya tenía un evento previo no-`Delete` del path) o
      `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` (nombre en eventos de directorio, `rename`-sobre-destino,
      `open_by_handle_at`+`CAP_DAC_READ_SEARCH`) queda para el track posterior: `nix` 0.30 no
      parsea los info-records de FID y el modo FID perdería el fd que hoy hashea el contenido.
- [x] Hash del contenido tras la mutación (`content_hash`) y de-dup idempotente.
      `hammer_journal::content_hash_of`/`hash_file` (blake3 plano, estilo `b3sum`, distinto del
      `of_inputs` de artefactos). `Journal::record_dedup` omite el evento si deja el archivo
      idéntico al último de ese path (o un `Delete` sobre algo ya borrado); `last_for_path` lo
      resuelve. El watcher hashea cada `CLOSE_WRITE` y usa `record_dedup`: una reescritura sin
      cambios ni ensucia el diario ni despierta el bus.

### Fase 4 — Formato y flujo `.swm`  ✅
- [x] `Swm` de/serialización YAML estable (roundtrip).
- [x] `verify_schema` (estructura + invariantes por mutación) y `verify_base` (distro_version
      + pins) con `BaseRef` / `BaseCompat`.
- [x] `apply_config_edit` (hunks `-/+` con búsqueda exacta, no-fuzz) y `apply_file_drop`
      (base64 inline + verificación BLAKE3 vs `content_hash`).
- [x] Bridge `Mutation::SourcePatch` → `Recipe` + `build` + hidratación.
- [x] CLI: `hammer apply [--prefix DIR] [--base-ref base.json]`,
      `hammer swm-verify`, `hammer export --journal DIR > out.swm`.
- [x] `patch_url` / `content_url` remotos. `hammer_build::download` (curl, sólo fuera del
      sandbox; acepta `file://` para tests offline). `build_source_patch` descarga `patch_url`
      al `swm-recipes/<commit>.patch` antes de compilar; `hammer apply` descarga `content_url`
      y **verifica BLAKE3 antes de escribir** (hash erróneo ⇒ nada tocado). Su integridad la
      cubre `content_hash` (file_drop) y el build reproducible + `expected_hash` (patch).
- [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que
      `hammer-build::build` escribe dentro del artefacto antes de sellar. `hammer export`
      agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta
      sea recuperable; lo demás cae al fallback `file_drop`. Source `git` **y `tarball`**
      modelados: `Mutation::SourcePatch` (y `RecipeInline`) llevan campos `repo`/`commit`
      **xor** `tarball`/`sha256` (resueltos por `swm::swm_source_kind`, misma regla que
      `recipe::Source::kind`); `hammer export` reconstruye ambos sin caer a `file_drop`.
- [x] Firma `signature` (ed25519) y `TrustStore` local. `hammer_core::sign`: `KeyPair`
      (genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`),
      `Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/
      `unsigned`) sobre bytes canónicos JSON del manifiesto sin la firma. CLI:
      `hammer keygen`, `hammer swm-sign`, y `hammer swm-verify --trust DIR`. La firma reporta
      autoría/integridad; NO autoriza promover (sigue mandando reproducir + commit). SDD 09 §3,§5.
- **Hecho cuando:** exportas un cambio, lo aplicas en otra máquina y reproduce idéntico.
      ✅ Demostrado en `crates/hammer-cli/tests/swm_roundtrip.rs` para
      `config_edit` + `file_drop`. `source_patch` reusa el camino de Fase 0/1 (gated en
      `HAMMER_NETWORK_TESTS`).

### Fase 5 — Bus de agente  ✅ *(queda `CRASHED` real, diferido al track posterior)*
- [x] `/run/init.control` (FIFO humano): `mkfifo`, reader que loguea cada línea, y
      `hammer ctl <line>` que escribe al FIFO con error claro si no existe.
- [x] `/run/agent.sock` (JSON-líneas) con handshake `Hello/Welcome`, auth via
      `SO_PEERCRED` y `CapsPolicy` por UID.
- [x] Comandos `Compile`/`Inject`/`Query`/`Init`; eventos
      `Welcome`/`BuildReady`/`BuildFailed`/`Injected`/`QueryResult`/`InitAck`/
      `Modified`/`Crashed`/`Error`.
- [x] EventBus in-process: el watcher publica `Modified` y todas las conexiones lo reciben.
- [x] Subsistemas independientes en `hammerd::main` (FIFO/watcher/bus en threads); si uno
      falla en init, los demás siguen.
- [x] Política expresiva leída de `/etc/hammer/agent-caps.toml` (`hammerd --agent-caps`).
      `hammer_core::AgentCapsConfig`: `default` + `[[rule]]` por `uid`/`gid` (primera que casa
      gana), `caps_for(uid,gid)`. `bus::policy_from_config` la enchufa; sin fichero o con
      fichero inválido, cae a la política por-UID por defecto (avisando). Ejemplo en
      `examples/agent-caps.toml`. El `default_policy` hardcoded queda como fallback.
- [ ] `CRASHED` real (requiere supervisión de servicios, que llega con el init propio del
      track posterior).
- [x] `BuildFailed.log_tail` con cola real del lab. `Sandbox::run` hace *tee* de
      stdout/stderr (sigue viéndose en vivo) y retiene las últimas 50 líneas; al fallar una
      fase devuelve `BuildFailure { reason, log_tail }` envuelto en `Error::Other`. El bus lo
      recupera por `BuildFailure::from_error` (downcast) y separa `reason` del log textual del
      compilador, para que la IA reaccione al error real, no sólo al mensaje de Rust.
- **Hecho cuando:** un cliente externo dispara un build y recibe el evento de fin por el
  socket. ✅ Camino implementado (`Compile` → `BuildReady`/`BuildFailed`) y la maquinaria
  alrededor cubierta por `crates/hammerd/tests/bus_e2e.rs`: handshake con peer creds,
  gating `no_cap`, `Query`, `Modified` fan-out, `Init`→FIFO. Un `Compile` real reusa el
  camino de Fase 0/1 (gated en `HAMMER_NETWORK_TESTS`).

### Fase 6 — Integración de la IA  ✅
- [x] Crate `hammer-agent` con tres piezas:
      - `AgentClient`: cliente síncrono del bus (handshake, `compile`/`inject`/`query`/`init`
        bloqueantes, drenado de eventos asíncronos).
      - `IntentTranslator` (trait) + `MockTranslator` cargado desde un `IntentCatalog` YAML
        (intent → `.swm` pre-armado). La integración con un LLM real se enchufa detrás del
        mismo trait sin tocar el bucle.
      - `Orchestrator` con `run(intent) → Proposal`: plan → schema → base → try (overlay
        o prefix) → apply (mutaciones puras + source_patch vía bus opcional) → verify →
        propose.
- [x] CLI: `hammer ai <intent> --catalog F [--prefix DIR --base-ref F --bus SOCK]`.
- [x] Tipos del protocolo del bus movidos a `hammer-core::proto` para que `hammerd` y
      `hammer-agent` los compartan.
- [x] Tests:
      - 10 unit en `hammer-agent` (translator + catalog + orchestrator).
      - 3 e2e del bucle agéntico (prefix tmp → archivos esperados en disco).
      - 1 e2e del cliente contra un *stub* del bus (handshake + Compile→BuildReady + Modified
        asíncrono).
- [x] Traductor LLM real (Claude API u otro), opcional vía feature flag o crate aparte.
      `ClaudeTranslator` detrás de la feature `llm-claude`, con `ureq + rustls`. Modelo
      por defecto `claude-opus-4-8`, adaptive thinking + `effort=high`. Trait `HttpClient`
      inyectable para tests sin red. CLI: `hammer ai --llm` (mutuamente excluyente con
      `--catalog`). Sin la feature, el flag falla con mensaje claro.
- [x] Lenguaje de consulta del sistema (SDD 08 §6) para que la IA refiera servicios y
      archivos sin rutas frágiles. Forma `kind:value` (`bin`, `file`, `pin`, `service`,
      `depends`), evaluable local (`hammer query <expr>`) y remoto vía bus
      (`Command::Query{what:"expr"}`). Parser ELF64 mínimo para extraer `DT_NEEDED`.
- [x] Bucle de auto-reparación (cliente reacciona a `Crashed` con un nuevo plan).
      `Orchestrator::run_with_repair(intent, &RepairPolicy)` drena async events del bus
      tras cada apply; si llega `Crashed`, formatea una nueva intención y reentra hasta
      `max_attempts`. El `Proposal` final lleva el `repair_chain` completo para que el
      humano vea la evolución antes de hacer commit. CLI: `hammer ai --repair-max-attempts`.
- **Hecho cuando:** una intención en lenguaje natural produce un cambio probado en overlay,
  presentado para `commit` humano. ✅ Demostrado por `hammer ai` con `MockTranslator`:
  intent → `.swm` → mutaciones aplicadas → `Proposal` con `overlay_id` + checks. El humano
  decide `commit`/`discard`. El upgrade a LLM real reusa todo el bucle.

---

## Track posterior — distro propia

Diseño completo en [SDD 11 — Bootstrap from-scratch](11-bootstrap.md). Resumen:

- **Bootstrap from-scratch** en tres etapas ([SDD 11 §3](11-bootstrap.md)):
  - **Stage 0** ✅ — crate `hammer-bootstrap` + `hammer bootstrap stage0`: ingiere el toolchain
    semilla pinned (`SeedSpec`, hash por identidad), verifica sha256 antes de sellar,
    idempotente. Cubierto por 4 unit + 1 e2e de CLI (offline, `file://`).
  - **Stage 1** ✅ — userland mínimo, **booteado end-to-end en QEMU**. Recetas pinned `musl` 1.2.5 +
    `busybox` 1.36.1 (estáticas, zig cc) + `hammerd` + `arje-zero` (recetas Cargo: repo pinned +
    deps vendoreadas en el fetch, build nativo crt-static vía `cargo rustc`); `hammer bootstrap
    stage1` los construye con la semilla, ensambla el rootfs FHS y lo sella. El init es **arje-zero
    como PID 1** (`/sbin/init`→arje-zero) con su seed card `/ente/seed.card.json`; al bootear,
    arje-zero carga+valida la seed y **supervisa hammerd + getty** (el `agent.sock` y el watcher
    fanotify de hammerd levantan). El **`CRASHED` real está demostrado**: `kill hammerd` → arje lo
    detecta (`Killed(SIGTERM)`), programa restart con backoff y lo re-encarna — la supervisión del
    init propio que la Fase 5 difirió ([ADR 0007](adr/0007-arje-como-init-propio.md)). Diseño en
    [SDD 12](12-init-real.md); corrida y fixes en el [runbook](runbooks/stage1-vm-boot.md). Pendiente:
    bus único (B.2: exponer el `CRASHED` a la capa de IA) y atestación (A1/A2).
  - **Stage 2** ✅ — rebuild nativo dentro del rootfs y diff de hashes ⇒ **auto-alojamiento
    bit-reproducible CONFIRMADO end-to-end (host↔VM)**. `KVM=1 MEM=24576
    ./scripts/selfhost-verify.sh` reconstruye los 4/4 dentro del rootfs Stage 1 con sólo su
    propio toolchain (arje-zero cu=1 compila en ~27 min in-VM), sella el rootfs y su
    `of_tree(stage1') = b3:0039b2b9…` **iguala** la referencia ⇒ `✓ REPRODUCIBLE: stage1' ==
    stage1`. El último no-determinismo (#3, `arje-zero`/`codegen-units`) se cazó y eliminó
    imponiendo `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1` en el sandbox (commit `4c9bcc0`).
    Corrida y veredicto en el [runbook](runbooks/stage1-vm-boot.md) §8.
- Reemplazar el init de Alpine por **tu init** (bus por pipes nativo) — habilita el `CRASHED`
  real que la Fase 5 dejó diferido. **Decisión: adoptar `arje`** (init from-scratch ya existente
  en el monorepo `tawasuyu`, con PID 1 + supervisión real) en lugar de escribir uno nuevo —
  ver [ADR 0007](adr/0007-arje-como-init-propio.md). Trae bus único, CAS BLAKE3 compartido y
  confianza en capas (procedencia hammer + atestación arje).
- Userland propio (busybox/toybox a elección), `/etc/service/*` propios.
- Decidir particionado/montaje de `/store` y `/var/lib/hammer`.
- Log de transparencia para compartir en comunidad ([SDD 09](09-trust-model.md) §4): el
  `bootstrap.json` de [SDD 11 §4](11-bootstrap.md) es su embrión.
- Mini-lenguaje de consulta del sistema para la IA ([SDD 08](08-ai-integration.md) §6).

---

## Estado actual
- ✅ **Fases 0–6 cerradas** sobre Alpine: build determinista → hidratación → overlay
  (con anidación LIFO) → diario (con `op` Create/Edit/Delete) → `.swm` firmado → bus de
  agente → bucle agéntico con IA (mock + Claude real tras `llm-claude`).
- ✅ El bucle completo está validado: una intención en lenguaje natural produce un cambio
  probado en overlay, presentado para `commit` humano.
- ✅ 214 tests verdes en el workspace (1 e2e de overlayfs gated en `HAMMER_OVERLAY_TESTS`;
  los de red en `HAMMER_NETWORK_TESTS`).
- ⏳ Único ítem de las fases diferido: `CRASHED` real (necesita supervisión de servicios →
  init propio del track posterior).
- ✅ **Track posterior arrancado y con su hito mayor cerrado:** bootstrap from-scratch
  Stage 0 → Stage 1 (booteado en QEMU, arje-zero como PID 1, `CRASHED` real) → **Stage 2
  auto-alojamiento bit-a-bit confirmado** (`✓ REPRODUCIBLE`, host↔VM). Baseline reproducible
  4/4: `of_tree(stage1)=b3:0039b2b9…` (cu=1).
- ✅ **CERRADO — auto-alojamiento *puro* (variante b, [SDD 11 §7.2b](11-bootstrap.md)):** que
  hammer construya el toolchain del builder desde fuente (no Alpine), reemplazando piezas una a una
  con Stage 2 verificando cada paso. **Pieza 1 hecha:** GNU make `4.4.1` (`recipes/make.toml`) se
  compila desde fuente con el lab — estática musl, sellada `b3:fbad44ac…`, reproducible bit-a-bit.
  **Swap implementado:** `BuilderSpec.swaps` + el flag `hammer bootstrap builder --swap make=<hash>`
  montan el `make` hammer sobre `/toolchain/usr/bin/make` (pisando el de Alpine) y lo anclan en el
  **hash lógico** del builder ⇒ la procedencia deja de ser "todo Alpine" y se vuelve auditable
  (pura `b3:8a370f5d…` vs swapped `b3:e7e2282c…` en el host). El verify lo expone con `SWAP_MAKE=1`.
  **Stage 2 in-VM con el swap (2026-06-13): `✗ DIVERGENTE` → causa raíz hallada y arreglada, make
  inocente.** El swap dio `of_tree(stage1')=9adefb82… ≠ 0039b2b9…`, pero el control **sin** swap dio el
  *mismo* `9adefb82…` ⇒ el make no era la causa. Bisección (descartando uno a uno: musl/busybox idénticos
  con hammer-make incluso a `-j1`; hammerd reproduce): el único flaky era **arje-zero**, no-determinista
  **incluso host-a-host** (~62 KB de diferencia, mismo rustc/commit/flags) — el backend **paralelo** de
  rustc/LLVM (ThinLTO) que `codegen-units=1` NO serializa. Builds seriales (la VM tiene 1 vCPU, o
  `taskset -c 0`) reproducían bit-a-bit a `9adefb82…`; los paralelos divergían. El viejo `0039b2b9…` era
  un build paralelo no-fiable. **Fix:** `CARGO_BUILD_JOBS=1` en el sandbox (serializa el jobserver ⇒
  backend de 1 hilo, independiente del nº de CPUs) — verificado: arje-zero a todas las CPUs + `JOBS=1`
  da byte-idéntico al serial. Baseline del host refrescado y `EXPECT_REF` re-baseado a `9adefb82…`.
  **Pieza 2 — busybox (sh+coreutils) validada en host:** el `/toolchain` Alpine es mixto (busybox para
  sh/sed/grep/awk/tar/find, GNU coreutils para cp/mkdir/install); el swap monta el busybox estático de
  hammer sobre `/toolchain/bin/busybox` (`--swap busybox=<hash>:bin/busybox`, sin código nuevo).
  Con make+busybox de hammer los 4/4 (incl. busybox compilándose a sí mismo con hammer-busybox de shell)
  reproducen `of_tree=9adefb82…`. Expuesto con `SWAP_BUSYBOX=1`. **✓ REPRODUCIBLE in-VM con ambos swaps
  (2026-06-13):** `KVM=1 MEM=24576 SWAP_MAKE=1 SWAP_BUSYBOX=1 ./scripts/selfhost-verify.sh` en `libre`
  reconstruyó los 4/4 dentro de la VM con el `/toolchain` hammerizado (make `fbad44ac…` + busybox
  `56664d70…` pisando los de Alpine) y el `of_tree(stage1')` **igualó** la referencia `9adefb82…` ⇒
  `✓ REPRODUCIBLE: stage1' == stage1`, `DRIVER_RC=0` (~54 min, arje-zero cu=1 in-VM). **La procedencia
  del builder ya no es "todo Alpine" y el auto-alojamiento sigue bit-a-bit.** **Pieza 3 — linux-headers
  (`recipes/linux-headers.toml`, kernel 6.16.12, validada en host):** los headers UAPI que musl/busybox
  `#include`. Construidos con `make headers` (NO `headers_install`: su `rsync` final falta en el toolchain
  hermético) + `unifdef` vía HOSTCC=zig cc. El swap es de **directorio**, no de binario: se extendió
  `assemble_builder` para reemplazar árboles enteros (los 13 subdirs kernel-owned `usr/include/{linux,asm,
  asm-generic,cxl,fwctl,misc,mtd,rdma,regulator,scsi,sound,video,xen}`), dejando intactos los de musl
  (bits/sys/net…). Hallazgo clave: el paquete de Alpine NO es el `make headers` crudo — trae 5 archivos
  distintos a la salida vainilla; 3 de ellos (`scsi/{scsi,scsi_ioctl,sg}.h`, legacy userspace, NO UAPI)
  son **requeridos** porque el applet `eject` de busybox los incluye y sin ellos no compila. Se aportan
  content-pinned vía `recipes/linux-headers-alpine-compat.patch` (+ 2 cosméticos: kernel.h/if_tunnel.h) y
  se pisan sobre la salida. Criterio de éxito fuerte alcanzado: **`diff -r` del header-tree hammer contra
  el de Alpine = vacío** ⇒ el toolchain swapeado es byte-idéntico al baseline ⇒ todo lo que compile abajo
  reproduce de por sí (garantía por construcción, más fuerte que un rebuild). Expuesto con
  `SWAP_LINUX_HEADERS=1`; ensamblado end-to-end en host OK. Falta sólo la corrida in-VM para el sello.
  **Pieza 4 — bwrap (`recipes/bwrap.toml`, bubblewrap 0.11.0, validada en host):** EL sandbox del propio
  lab (`hammer-build/src/sandbox.rs`). Binario estático ⇒ swap de archivo sobre `/toolchain/usr/bin/bwrap`.
  Como tool del toolchain (no input del 4/4) NO necesita casar byte-a-byte con Alpine, sólo aislar igual.
  Tres sub-hallazgos que la hicieron más jugosa que make/busybox: (1) el toolchain Alpine no trae meson/
  ninja/python — bypaseamos meson compilando los 4 `.c` de bubblewrap directo con `zig cc` (+`config.h`
  trivial generado a mano); (2) depende de **libcap**, cuyo -dev (libcap.a + `sys/capability.h`) tampoco
  está en el toolchain ⇒ nueva receta `recipes/libcap.toml`; (3) para que el build de bwrap viera libcap se
  agregó **materialización de build-deps** al lab: `deps.build` ahora se construye recursivamente y cada
  artefacto sellado se apila como capa `--overlay-src` BAJO el rootfs del sandbox, dejando sus
  `usr/{include,lib,lib/pkgconfig}` en `/usr` — pkgconf y zig cc los hallan sin plumbing de flags (recetas
  sin deps quedan byte-iguales, baseline intacto). Validación host fuerte: hammer-bwrap es estático, corre
  `--version`/sandboxea, y **musl rebuildeó byte-idéntico** usándolo de sandbox (bisección). Expuesto con
  `SWAP_BWRAP=1`; falta sólo correrla in-VM.
  **Pieza 5 — coreutils (`recipes/coreutils.toml`, GNU 9.8, validada en host):** cp/mkdir/ln/chmod/mv/
  install que el `make install` de musl/busybox usa. Empaquetada multicall (`--enable-single-binary`,
  igual layout que Alpine: un `/bin/coreutils` + ~100 symlinks), así **un solo swap del binario** rutea
  todos los applets. Tool, no input ⇒ build vainilla sin patches. Bisección host fuerte: **musl Y busybox
  rebuildearon byte-idéntico** (`57b66a2e`, `56664d70`) con hammer-coreutils pisando el de Alpine.
  Expuesto con `SWAP_COREUTILS=1`. Falta correrla in-VM.
  **Herramientas de toolchain extra desde fuente (provenance, no en el camino del 4/4 mínimo):**
  `recipes/{patch,m4,pkgconf}.toml` — GNU patch 2.8 (lo usa `apply_patches`), GNU m4 1.4.20 (base
  autotools) y pkgconf 2.5.1 (lee los `.pc` que materializa el lab). Construidas estáticas con zig cc
  y validadas (corren, versión correcta, funcionales). El 4/4 mínimo (musl/busybox/hammerd/arje-zero)
  no las invoca, así que no llevan flag `SWAP_*` dedicado: son swapeables con el escape `SWAPS="name=hash:rel"`.
  Avanzan el "builder reconstruible al completo, no todo-Alpine" aunque el verify no las ejercite hoy.
  **✅ CORRIDA in-VM acumulada (2026-06-14): los 5 swaps de sandbox juntos `✓ REPRODUCIBLE`.**
  `KVM=1 MEM=... SWAP_MAKE=1 SWAP_BUSYBOX=1 SWAP_LINUX_HEADERS=1 SWAP_BWRAP=1 SWAP_COREUTILS=1
  ./scripts/selfhost-verify.sh` reconstruyó los 4/4 dentro de la VM con el `/toolchain` hammerizado y
  el `of_tree(stage1')` igualó `9adefb82…` bit a bit (~46 min, sobrevive presión de memoria). Recetas
  extra de toolchain desde fuente añadidas después: `recipes/binutils.toml` (GNU binutils 2.45.1, CC=gcc
  porque zig miscompila → segfault, link dinámico, inerte al of_tree porque zig ya provee as/ld/ar;
  swapeable vía `SWAPS=`).
- ✅✅✅ **CERRADO — frente rust/llvm (la última pieza grande del toolchain):** cadena purista
  `mrustc → rustc 1.90.0 → 1.91.0 → 1.91.1` con `x.py` real, reusando un LLVM 20.1.8 externo en todo
  (sin rebuild). `rust-1.91.1-prefix/bin/{rustc,cargo}` corren standalone (rpath, host musl), versión
  EXACTA de Alpine. **Auto-consistencia VERIFICADA in-VM (2026-06-18):** `SWAP_RUST=1 KVM=1
  PRESEED=hammerd ./scripts/selfhost-verify.sh` → `✓ REPRODUCIBLE` bit a bit (host y VM dan
  `of_tree=7fa6cb4e…` = `RUST_EXPECT_REF`, ≠ `9adefb82…` Alpine). Para el rebuild bit-reproducible
  se committeó el `Cargo.lock` de arje-zero en tawasuyu + se bumpeó la receta (`--locked`); `PRESEED=hammerd`
  es OBLIGATORIO (arje-zero vendorea ~1973 crates y un rebuild in-VM completo desborda RAM). Artefactos en
  `scripts/rust-frontier/`.
- ✅✅✅ **CAPSTONE (2026-06-19): LOS 6 SWAPS JUNTOS `✓ REPRODUCIBLE` IN-VM** — los 5 tools de sandbox +
  `SWAP_RUST` a la vez, `of_tree=7fa6cb4e…` bit a bit (~7 min, `MEM=12288 PRESEED=hammerd`, sin swap en el
  host). El auto-alojamiento del toolchain del builder está COMPLETO; las piezas inertes al 4/4
  (binutils/pkgconf) quedan salteadas por diseño. Capa de arranque restante (status libc, no vale
  de-Alpinizar): gcc, m4, libz/libzstd runtime.
- ✅✅✅ **CERRADO — kernel-from-source ([SDD 11](11-bootstrap.md), último eslabón de soberanía):** el
  kernel Linux 6.16.12 construido por hammer (`recipes/linux.toml`, `CC=gcc`, monolítico
  e1000/overlay/userns=y) **BOOTEA la VM del selfhost-verify** y el rebuild in-VM da `✓ REPRODUCIBLE`
  bit a bit (`HAMMER_KERNEL=1 KERNEL=<bzImage> KVM=1 MEM=16384 PRESEED=hammerd`). Config lean (build
  ~35→~14 min, bzImage 13→8.7 MB). El muro `pivot_root` (`/` = rootfs absoluto del mount-ns ⇒ bwrap no
  pivota; quirk que el kernel host enmascaraba) se resolvió con un `/init` wrapper `switch_root` a tmpfs
  (condicional a `HAMMER_KERNEL=1`, no toca el camino del kernel host). **Build-deps del kernel TODOS
  de-Alpinizados:** `recipes/{flex,bison}.toml` (kconfig), `recipes/openssl.toml` (3.5.4, certs/extract-cert),
  `recipes/elfutils.toml` (sólo libelf 0.194 para objtool, con shims musl mínimos). Capa de arranque
  restante: m4, libz/libzstd, gcc (status libc).
- ✅ **Etapa A — orquestador `bootstrap all` + log de transparencia exportable:** `hammer_bootstrap::all`
  encadena stage0→stage1→stage2 en una corrida del host (`hammer bootstrap all --url … --sha256 … --version …`)
  y deja el `bootstrap.json` poblado; `hammer bootstrap manifest` lo imprime (la superficie de publicación
  del log, SDD 11 §4). El veredicto `✓ REPRODUCIBLE` lo sigue sellando el rebuild in-VM (`selfhost-verify.sh`),
  no el host — `all` ancla la referencia para que ese rebuild la compare. **Pendiente de la Etapa A:** el
  smoke end-to-end de I3 (bus único) contra un init **vivo** (arje+hammerd corriendo) — no reproducible
  sin la VM, mismo status que cualquier sello in-VM.
- ✅ **B.2 — `CRASHED` real a la capa de IA (cableado end-to-end):** el `Event::Crashed` del
  bus de agente ya tiene fuente real. **Fuente (arje):** `arje-bus` ganó `BusRequest::Subscribe`
  + `BusPayload::Event(BusEvent)`; arje-zero difunde en `on_death` `EnteCrashed{id,label,status}`
  / `EnteRestarting{delay_ms}` / `EnteExited` a las conexiones suscritas, purgando las muertas
  (4 tests). **Sink (hammer):** `hammerd::crashes` traduce la señal normalizada → `Event::Crashed`
  → `EventBus` → `/run/agent.sock` (3 tests). **Transporte (hammer):** `hammerd::arje_link` se
  suscribe al bus de arje (`$ENTE_BUS_SOCK`) y reenvía cada `BusEvent`. Decisión de wire =
  **relectura del frame postcard** (mirror mínimo del subconjunto de suscriptor; no arrastra el
  crate-graph de arje ⇒ hammer sigue standalone). El layout está **verificado byte-a-byte contra
  `arje-bus` real** (mismas versiones ulid 1.2 / postcard 1.1; frame Subscribe = `[00,01,00,0d]`)
  y cubierto por un round-trip local (2 tests). Resta sólo el smoke end-to-end contra un init
  vivo (no reproducible sin arje corriendo).
- ⏭️ **También pendiente (Stage 1):** atestación arje (A1/A2 — ya cableada en el repo arje,
  resta sólo enchufar su veredicto de boot a este roadmap).

## Notas de entorno
- Desarrollo principal: laptop del autor.
- Host actual: Proxmox (`gioser.net`). La VM/LXC Alpine de pruebas vivirá aquí o en la laptop.
- Repo: `https://gitea.gioser.net/sergio/hammer`.
