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>
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).
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>
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>
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>
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>
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>
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>
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.
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>
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>
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>
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>
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>
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>
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>
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.
`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.
`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).
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>
Nuevo crate hammer-overlay + integración al CLI. Diseño:
- API: try_overlay(targets, state_root) → OverlayId, status, discard, commit.
- Layout: <state>/<id>/{state.json, mounts/<slug>/{upper,work}}. El manifiesto
permite a `status` enumerar overlays sin estado en memoria.
- commit: desmonta primero, luego cp -a upper→lower por archivo, procesa
whiteouts (char dev 0/0) como remove en el lower. Sin diario aún (Fase 3).
- discard: umount LIFO + rm del state. Idempotente sobre `not mounted`.
- Privilegios: la librería no asume; emite `mount`/`umount` y deja que el
caller los tenga. CLI sin sudo falla con mensaje claro. El daemon de
Fase 5 los obtendrá desde contexto privilegiado.
Tests:
- 6 unit tests (slug, defaults, manifest, errores).
- E2E bajo bwrap+user-ns con CAP_SYS_ADMIN: ciclo try → escribir → commit →
verificar persiste; try → escribir → discard → verificar revertido;
remove en merged → commit → whiteout aplicado al lower. Gateado en
HAMMER_OVERLAY_TESTS=1: por defecto, los kernels deniegan el mount
overlayfs en user-ns no-privilegiado (sysctl/AppArmor); el dev habilita
cuando su entorno lo permite o corre con sudo.
CLI: hammer try [targets…] --state-root, commit <id>, discard <id>, status.
Refactor mínimo: las cmds que no tocan artefactos ya no abren (ni crean) el
store. `hammer status --state-root /tmp` no toca /store.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `fetch_tarball.rs` (5 tests, sin red ni rootfs): genera tar.gz local, lo sirve
vía `file://` y ejercita el path completo: strip_components default vs 0,
sha256 mismatch que NO envenena caché ni deja .partial, reuso de caché en
segunda llamada con la fuente original eliminada, y rechazo de un archivo
cacheado con contenido distinto al sha esperado.
- `end_to_end_grep.rs` (1 test, gated): grep 3.12 desde GNU, build + hidratación
+ ejecución en bwrap+Alpine + cache-hit en segunda build. Doble skip:
HAMMER_NETWORK_TESTS!=1 o .dev-fs sin bootstrap. Comparte .dev-fs/cache con la
dev para no repagar musl.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cierra el primer entregable del roadmap. Cambios:
- `Source`: ahora admite dos modos mutuamente excluyentes — `git` (repo+commit) o
`tarball` (url+sha256). El hash de entrada del artefacto usa el commit en modo
git y el sha256 en modo tarball; ambos son identificadores inmutables del
contenido fuente. Validación en `Source::kind()` con error claro.
- `fetch`: dispatch por modo. Tarball cacheado en `work/tarballs/<sha>.tar`,
descarga vía `curl -fL`, verificación sha256 con `sha2`, extracción con
`--strip-components` (default 1, GNU-style).
- `recipes/grep.toml`: GNU grep 3.12 desde el tarball release de gnu.org
(sha256 fijado, sin patches, `--disable-perl-regexp --disable-nls`).
- Bootstrap: añade `binutils` al rootfs Alpine — configure de autotools mira
ld/ar/ranlib incluso cuando el compilador real es zig cc.
- `.gitignore`: `/work/` (artefactos transitorios del build).
- `docs/10-roadmap.md`: Fase 0 y Fase 1 marcadas ✅; entregable cerrado.
Nota sobre v3.11 vs v3.12: probé primero v3.11 y el binario producido daba
"memory exhausted" en cualquier regex (bug conocido de grep+musl static al
inicializar DFA). v3.12 corrige y funciona limpio dentro del rootfs Alpine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `BuildConfig.cache_root` (override `HAMMER_CACHE`): bindea `/cache` al sandbox
y exporta `ZIG_GLOBAL_CACHE_DIR=/cache/zig`. Evita ~30s de recompilación de
musl en cada build.
- `scripts/bootstrap-devfs.sh`: idempotente, descarga+verifica alpine-minirootfs
3.23.4 y zig 0.16.0 con sha256 fijo, instala build tools en el rootfs vía apk
bajo bwrap.
- `make_tree_read_only`: solo archivos regulares pierden `w`; los directorios
conservan 0o755 para no estorbar GC ni `rm -rf` administrativo. La
inmutabilidad estricta del store se delega al mount RO de la distro propia.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
detect_build_system inspecciona el árbol del source y elige los comandos:
- configure.ac sin configure → autoreconf -fi && ./configure --prefix=/usr
- configure presente → ./configure --prefix=/usr
- CMakeLists.txt → cmake -B _build ... ; cmake --build ; cmake --install
- meson.build → meson setup _build ... ; meson compile ; meson install
- Makefile / GNUmakefile → make -j && make install DESTDIR=/out PREFIX=/usr
- nada → la receta debe traer [build.phases]
Overrides en la receta siguen ganando. recipe.build.flags se concatenan al
configure. Cuando link = "static", el orquestador inyecta LDFLAGS=-static
para que el link final autotools/cmake produzca el binario monolítico que
quiere nuestra hidratación primaria.
Tests:
- 7 unit tests de detección + emisión de comandos por sistema.
- 1 integration test end-to-end con autotools real (configure.ac + Makefile.am
+ hello.c → autoreconf+configure+make+install dentro del sandbox, sellado,
ejecutado bajo bwrap). Requiere el rootfs Alpine con `apk add make
autoconf automake m4 patch coreutils libtool pkgconf bash`.
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>