# 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 ` → 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 --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.json`; `status` enumera. - [x] `commit` promociona archivos y procesa whiteouts (char dev 0/0) como remove. - [x] Subcomandos del CLI: `hammer try [targets…]`, `commit `, `discard `, `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/.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` modelado; `tarball` cae a file_drop con warning (pendiente extender SourcePatch). - [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 ` 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 --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 `) 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). - 🚧 **En curso — 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=` 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 corrido (2026-06-13):** dio `✗ DIVERGENTE` (`of_tree(stage1')=9adefb82… ≠ 0039b2b9…`). **Pero el make es inocente:** el diagnóstico en host (musl+busybox con hammer-make en un store desechable ⇒ byte-idénticos al baseline vía `diff -r`, y el ensamblado completo ⇒ `of_tree` EXACTO `0039b2b9…`) prueba que hammer-make reproduce el baseline. La divergencia es **no-determinismo in-VM** que coincidió con una corrida bajo presión de memoria fuerte (97 min wall para ~27 min de compute, swap thrashing), no el swap. Pendiente: re-correr el verify **sin** swap (control) para confirmar la flakiness in-VM independiente del make (sospechoso: arje-zero o la presión de memoria), cerrarla, y *después* validar la pieza 1; luego make/autotools → linux-headers → bwrap → rust/llvm. - ⏭️ **También pendiente (Stage 1):** bus único (B.2: exponer el `CRASHED` a la capa de IA) y atestación arje (A1/A2). ## 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`.