Encontrado instalando `takana` desde su propio repo (SDD 28 §5), que es la primera vez que se hace
el viaje completo receta → `.tkn` → `install --require-signed` sobre un paquete con `strip_debug`.
Error: expected_hash no coincide:
declarado = b3:941d1857e6e93ebbfbad8240a1c30405263e5ff8fc881a923aee312160563eea
obtenido = b3:2727050ebdbd7817b53e79ffe9796569e9364454ed35474b51e881fcc6048766
`why-differs` sobre los dos artefactos lo nombró: las recetas selladas diferían en `version`,
`license` y **`strip_debug`**. Los dos binarios pesaban EXACTAMENTE lo mismo (3.374.768 bytes) y sólo
divergían en `.shstrtab` — la firma de un `strip` que corrió una vez y la otra no.
`swm_bridge` re-inyecta con cuidado `flags`, `phases`, `zig_version`, `strip_components`, `patches`,
`deps`, `evidence` y `slots` («fidelidad de reconstrucción», dice su comentario) y **se olvidaba de
`strip_debug`**, que ni siquiera existía en `SwmBuild` ⇒ no viajaba en el `.swm` en absoluto. Como
ENTRA EN `hash_inputs` (`recipe.rs:572`, con el comentario «cambia el CONTENIDO del artefacto, así
que TIENE que entrar»), el receptor reconstruía con `None` y sellaba en otra dirección.
**Alcance medido: 40 recetas del corpus lo usan, 18 de las 88 del perfil `servidor`.** Para todas,
el `expected_hash` anclado por `pack --build` no coincidía NUNCA y `install --require-signed`
abortaba. O sea: una quinta parte del repo no se podía instalar, y nadie lo sabía porque nunca se
había cerrado el lazo.
Por qué no lo cazó nadie antes: `tree` —sin `strip_debug`— da cache-hit y funciona perfecto. El bug
sólo aparece en las recetas que lo declaran, y el dogfood previo no las tocaba.
Test de regresión con su control: una receta CON `strip_debug` lo lleva en el `.swm`, y una SIN él no
gana un `false` inventado. 221 tests de takana-core en verde.
Verificado end-to-end tras el arreglo: `install takana --repo http://<caja> --require-signed` da
cache-hit en `b3:941d1857…` —el mismo hash anclado— e hidrata los 3 ficheros.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
205 lineas en 73 ficheros de crates, mas la prosa de CLAUDE.md y del skill,
que se me habian quedado afuera de los barridos anteriores (no eran ni recetas
ni docs/ ni scripts/).
EL BARRIDO ANCHO ESTUVO A UN COMMIT DE ROMPER EL CORPUS ENTERO.
El primer intento reescribia los .rs completos, no solo los comentarios. Entre
las lineas de codigo que tocaba estaban SIETE etiquetas de separacion de
dominio, que son ENTRADA DE HASH:
b"hammer-tree-v1" <- el prefijo de ArtifactHash::of_tree (hash.rs:60)
b"hammer-seed-v1" la funcion que hashea TODOS los artefactos:
b"hammer-stage1-rootfs-v2" cambiarla mueve los 4750 hashes del store
b"hammer-product-rootfs-v3"
b"hammer-product-attested-v2"
b"hammer-builder-rootfs-v1"
b"hammer-attest-dev-rootkey-0001!!" <- clave raiz de atestacion, [u8;32]
Revertido y rehecho solo sobre comentarios, esquivando ademas las cadenas
crudas de Rust (r#"..."#) porque el SYSTEM_PROMPT del traductor tiene lineas
que empiezan como comentario.
Controles: las 7 etiquetas siguen ahi, el diff toca CERO lineas de codigo,
605 tests en verde y el hash de zlib sigue en b3:dc363f26.
La leccion es la misma de toda esta etapa: un literal que parece prosa puede
ser entrada de hash, y la unica forma de saberlo es mirar donde se usa.
TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.
VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.
DOS BINARIOS SE CONGELAN, y no por prolijidad:
- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
`PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.
- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
instalados y hornea un hook de arranque que lo invoca por ese nombre:
renombrarlo rompe máquinas instaladas, no el repo.
Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.
Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
arje inserto StopCardFromDisk en el medio de BusRequest el 2026-06-26 y
corrio Subscribe a indice 14 — el puente estaba suscribiendose con el byte
viejo desde entonces (roto en silencio; lo delato el golden test de arje-bus
el 2026-07-16, lado tawasuyu 41f4accc7). Ademas el espejo de BusEvent no
conocia EnteParked/EnteRefloored: se agregan como consumo-sin-senal
(to_lifecycle -> Option). Tests unit+e2e actualizados y verdes.
Sube el modelo de slots de wawa-memo (prototipo host) a hammer:
- Recipe + source_patch del .swm llevan bloque `slots` {claims, requires} (slot->b3:…),
FUERA de hash_inputs (topología ≠ identidad, no mueve el artifact_hash). Viaja intacto
por los dos sentidos del puente (Recipe->.swm->Recipe): test de round-trip.
- InstalledDb registra claims por paquete + system_state() -> slot->hash (el Estado del gate).
- hammer-core::compat::evaluar(estado, slots) -> Veredicto {Compatible, Colision, Incompatible}
(el álgebra probada en wawa-memo, sobre tipos de hammer).
- Gate en `hammer install`: antes de tocar nada evalúa el paquete contra el estado instalado.
Incompatible (requisito sin resolver, caso wayland) -> aborta; Colisión (caso logo) ->
aborta pidiendo elección salvo --force-slots; Compatible -> procede y registra los claims.
Plumbing propagado por los 5 sitios de Mutation::SourcePatch (from_recipe, swm_bridge,
export, bus, orchestrator). Tests: compat (4) + slots-no-en-hash/round-trip (2) +
system_state (1) + puente receta<->swm (1). Workspace compila y verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La evidencia corre del lado del BUILD (hammer-agent no depende de hammer-build:
separación PROPONE/CONSTRUYE). Piezas:
- proto: RecipeInline lleva `evidence`; Event::BuildReady lleva
`verdict: Option<EvidenceVerdict>` (+ EvidenceCheckVerdict). Ambos con
skip_serializing_if ⇒ wire compatible con clientes pre-H1c.
- hammerd/bus: run_compile ejecuta la evidencia declarada tras sellar el
artefacto (run_evidence vía swm_bridge::recipe_from_source_patch) y adjunta
el veredicto en BuildReady. Si ni se pudo ejecutar ⇒ veredicto fallido
sintético (no pasa en silencio). El lab reporta; el gate vive en el agente.
- client: compile() devuelve CompileOutcome { artifact, verdict }.
- orchestrator: VERIFY lee el veredicto; si all_passed=false empuja
VerifyCheck::fail y ABORTA antes de hidratar (artefacto sellado, no toca el
sistema). Nuevo constructor VerifyCheck::fail.
Tests: roundtrip del veredicto en proto; stub-bus refleja evidencia→verdict;
nueva prueba de integración orchestrator_evidence_gate (veredicto fallido ⇒
run() aborta con "no se propone" y no hidrata). Suite completa verde (33 suites).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontera AI-nativa SDD 15 §H1. Agrega `Evidence { checks: Vec<EvidenceCheck> }` a Recipe
(TOML) y a Mutation::SourcePatch (.swm YAML): cada check es {kind, cmd, expected_exit,
expected_output?} con kind ∈ {cmd-exit, proptest, contract, kani} (estratos de confianza
crecientes, EvidenceKind: Ord). DECISIÓN CLAVE: la evidencia NO entra en hash_inputs —
certifica comportamiento, no identidad ⇒ no mueve el artifact_hash (baseline de
reproducibilidad intacto). Round-trip completo: from_recipe (forward) + swm_bridge
synthesize_recipe (reverse) + camino de pack (cli). verify_schema valida forma (cmd no
vacío, expected_output con prefijo b3:). El checker que EJECUTA la evidencia es H1b; el
cableado al Orchestrator VERIFY es H1c (marcado con evidence: _). Tests: recipe + swm,
incl. que la evidencia no cambia el hash. Sin warnings clippy nuevos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el lazo Etapa F+G: el userland del producto se arma vía `hammer install
--repo --prefix --require-signed` desde el repo firmado, no del bootstrap
hardcodeado. scripts/product-userland-from-repo.sh hidrata un set curado (13+
tools validados estáticos del repo).
Dos bugs reales del path install/pack encontrados+arreglados:
1. pack derivaba target_bin=/usr/bin/{name}; para paquetes con binario≠nombre
(ripgrep→rg, repgrep→rgr) quedaba mal y el sanity-check de install fallaba.
Ahora pack lo deriva del flag `--bin <X>` de la receta Cargo.
2. La reproducción del source_patch derivaba el NOMBRE de la receta del
target_bin ⇒ "rg" ≠ "ripgrep" del corpus ⇒ no cache-hit ⇒ rebuild + dup en
el store (find_by_hash ambiguo). build_source_patch/run_apply ahora reciben
el nombre del paquete (install/bootstrap lo pasan; apply=None) ⇒ cache-hit
del artefacto del corpus, sin duplicar.
(El "EACCES" inicial era sólo --store ausente: DEFAULT_STORE=/store root-only.)
hammer-build 49/49 tests ok; ripgrep install validado e2e (rg hidratado).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el último tramo de validación de B.2 (adoptar arje como init): el
adaptador arje_link ya estaba escrito y cableado en main, pero su único test
era un round-trip del espejo contra sí mismo — no probaba la ruta de transporte
real ni el contrato con arje-bus.
arje_link_e2e levanta un socket falso de arje-bus (mismo wire: frames
u32-BE-len + postcard), valida que el adaptador emite el frame Subscribe exacto
([00 01 00 0D]), le inyecta un EnteCrashed(Killed 11), y verifica que sale como
Event::Crashed{code:139} por el EventBus — ejercitando run() entero
(connect→subscribe→leer→traducir→publicar) sin necesitar un init vivo. run()
pasa a pub(crate) para que el test lo maneje. El contrato de bytes con arje-bus
queda fijado del lado arje por arje-bus::contrato_wire.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.
Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.
- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
+ base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"):
una receta que el sistema ya sabe construir se vuelve un paquete distribuible y
reproducible-desde-fuente, inversa de `hammer apply`.
- hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)`
(constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version`
y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para
reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13).
- hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada
phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico.
- hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`.
Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no
las modela = pieza posterior). `export` también enriquece su source_patch.
- Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases),
coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hammerd::arje_link se suscribe al bus del init (ENTE_BUS_SOCK), relee el frame
postcard de arje-bus con un mirror mínimo de suscriptor (sin arrastrar el
crate-graph de arje ⇒ hammer sigue standalone) y reenvía cada BusEvent →
crashes → Event::Crashed → agent.sock. Wire verificado byte-a-byte contra
arje-bus real (ulid string, frame Subscribe=[00,01,00,0d]); 2 tests de round-trip
local + frame. Se lanza en thread si ENTE_BUS_SOCK está definido (no-op si no).
Roadmap B.2 marcado ✅ (resta sólo el smoke contra init vivo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deja de ser un error "pendiente": hydrate(.., DynamicSpec{interpreter,rpath})
detecta ELF por magic, copia los que necesitan parcheo (no hardlink: patchelf
mutaría el store) y aplica --set-interpreter/--set-rpath atómicamente
(copy→patch→rename); no-ELF y spec vacío siguen hardlinkeando. ensure_patchelf
falla limpio antes de tocar el FHS si falta la herramienta. HydrateReport.patched
cuenta los reescritos. Callers (bus/cli/bootstrap/e2e) pasan None=estático.
4 tests nuevos (incl. camino de error sin patchelf y real gated). Doc §4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256
(Option, serde-default ⇒ compat con .swm git existentes), resueltos por
swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge
sintetiza la receta según el modo; hammer export reconstruye fuentes tarball
como source_patch en vez de caer a file_drop (provenance fina recuperada).
Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nuevo módulo hammerd::crashes: traduce la señal de ciclo de vida normalizada
(fuente = arje-bus BusEvent) a proto::Event::Crashed y la bombea al EventBus →
agent.sock (capa de IA). Traducción + bucle de bombeo desacoplados del
transporte y testeados (3 tests). Resta sólo el adaptador de transporte
arje-bus ↔ hammerd. Roadmap B.2 actualizado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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).
- 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).
- 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).
Nuevo crate hammer-journal y watcher fanotify en hammerd. Cierra el contrato
del SDD 05 §3 ("toda mutación trazable/opaca registrada y consultable") y
liquida la deuda Fase 2 sobre el hook al diario en commit.
hammer-journal:
- MutationEvent (op, path, by, content_hash?, note?), MutationOp
Create/Replace/Edit/Delete, Source enum tagged HammerHydrate /
HammerCommit / External (distingue trazable de opaco para `export`).
- Journal: record append-only + sync_data, read_all/tail/follow (offset),
resilencia a líneas malformadas (kernel-panic mid-write se ignora).
- RFC 3339 UTC sin chrono (Hinnant civil_from_days, válido para todo i64).
- 7 unit tests cubren append/read/tail/follow/malformed/serde/timestamp.
Watcher (hammerd):
- nix::sys::fanotify con FAN_CLASS_NOTIF + FAN_CLOSE_WRITE +
FAN_EVENT_ON_CHILD sobre /bin /sbin /usr/bin /usr/sbin /lib /usr/lib /etc.
- Resuelve path por readlink(/proc/self/fd/<n>) — sin FAN_REPORT_DFID_NAME
para Fase 3 v1; el refinamiento Create/Edit con metadata viene después.
- Filtro: hammer_overlay::status(state_root) → ignora paths bajo un overlay
activo (el ruido del experimento no entra hasta el commit).
- Sin CAP_SYS_ADMIN, fanotify_init falla; hammerd lo reporta y sigue
durmiendo (preparado para que Fase 5 monte el bus encima).
- 3 unit tests sobre defaults/filtros/uid lookup.
CLI:
- hammer journal [--dir] [--tail N] [--follow] [--format pretty|json].
- hammer commit registra cada archivo promocionado/removido vía
commit_with_journal; --no-journal opt-out.
Hook overlay::commit:
- commit_with_journal(id, root, journal): para cada copied/removed emite un
MutationEvent con Source::HammerCommit { overlay }. Test directo del hook
sin overlayfs (lógica pura) más el e2e existente que ya cubre los mounts.
57 tests workspace, 0 fallos.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- hammer-core: Recipe::from_toml/load_from_path reales; Phases override;
hashing por *contenido* de patches; Store::seal con rename atómico + chmod
r/o recursivo; Store::find_by_hash por prefix.
- hammer-build: fetch (git mirror + archive al commit fijado), Sandbox::run
sobre bwrap con rootfs Alpine como tmp-overlay y zig cc inyectado,
orquestador build con caché por hash, hydrate con hardlinks atómicos
(link tmp + rename) que pisa el FHS sin tocar inodes del store.
- BuildConfig leído de HAMMER_{ROOTFS,ZIG,WORK} o defaults relativos al store.
- CLI: hammer build / hammer hydrate funcionales; logs a stderr para que
stdout sea pipeable (el hash y nada más).
- 20 unit + 1 integration test end-to-end (hello.c estático compilado bajo
bwrap, sellado, hidratado, ejecutado en sandbox limpio).
Cierra el primer entregable del roadmap salvo "X = grep" (falta heurística
autotools/cmake en resolve_phases).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Arranque del proyecto hammer (distro AI-nativa: laboratorio funcional en el
sótano, terminal mutable clásica arriba, integración de IA programadora).
- Workspace Rust (compila, tests verdes): hammer-core, hammer-build,
hammer-cli (bin `hammer`), hammerd.
- SDDs 00-10 + 6 ADRs en docs/ con toda la arquitectura.
- Esqueletos navegables mapeados a las fases del roadmap; Fase 0/1 listas
para implementar el sandbox real.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>