Commit Graph
11 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 b979d9e550 Etapa G: importador nix→receta (hammer import-nix + scripts/nix-import.sh)
Poblar el catálogo no es opcional: 34 recetas a mano = userland desierto. nixpkgs es el mayor set
de recetas DESDE FUENTE ⇒ semilla natural. Importamos la RECETA (source+hash+deps), nunca el
binario del cache de nix — hammer reconstruye desde fuente ("verificar, no confiar").

- crates/hammer-cli/nix_import.rs: consume el JSON normalizado de nix y emite una receta hammer.
  Clasifica el origen: fetchurl flat → tarball+sha256 (convierte el hash nix SRI/base32/hex → hex);
  fetchFromGitHub → repo+commit (hammer pinea por commit, no necesita el hash NAR). Filtra el ruido
  de stdenv (setup-hooks, wrappers). nix_base32 decode portado. 11 tests.
- `hammer import-nix [FILE|-]` (stdin) → receta .toml; valida que parsee como Recipe.
- scripts/nix-import.sh <attr>: `nix eval --apply` produce el JSON normalizado y lo pipea al
  importador. NIX_STORE= para store local si /nix/store no es escribible.
- VALIDADO contra nixpkgs REAL (nix 2.34): import hello (tarball, sha256→hex) + ripgrep (github→
  repo+commit); pipeline completo nix→import→pack→.swm probado con hello. 31 suites verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:41:50 -04:00
sergioandClaude Opus 4.8 910db267ef Etapa F paquetería #6: repo sobre red — install --repo URL (HTTP/HTTPS)
El repo son ficheros estáticos (index.json + .swm) ⇒ cualquier servidor estático lo sirve.

- hammer-cli: `RepoSource` {Local(path) | Http(url)}. `install --repo` ahora acepta path o URL.
  Para HTTP: lee index.json por GET, materializa un repo LOCAL temporal bajando el índice + los
  .swm del cierre de deps (curl, vía download::fetch_url_bytes), y de ahí el flujo es IDÉNTICO al
  local (resolución de deps, verificación de release/firma/base, reproduce + hidrata). tempfile
  pasa a dep normal de hammer-cli.
- Validado E2E: server HTTP estático + install openssh vía http:// → "release: trusted" (índice
  firmado bajado por red) → "repo: bajados 3 .swm" (cierre openssh+zlib+openssl) → resuelve del
  temporal. (El proxy del sandbox exige NO_PROXY para localhost; el código es correcto.) 31 verde.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 07:07:32 -04:00
sergioandClaude Opus 4.8 d23946fa3d hammer-upgrade: upgrades atómicos con generaciones y rollback (Etapa E4, primer corte)
Crate hammer-upgrade + CLI 'hammer upgrade apply|rollback|status'. Aplica un
árbol Stage1/producto del store sobre el FHS vivo dejando una generación
(manifest + backup de bytes previos), atómico fichero-a-fichero con 'current'
como commit-point. Rollback restaura EXACTO al árbol anterior (o pre-upgrades).
Cada cambio al journal (HammerHydrate, replay-able). Verificación opcional del
of_tree esperado (índice de mirror E3 / release firmada). Maneja ficheros +
symlinks. 7 tests unitarios + scripts/upgrade-e2e-test.sh (apply v1->v2->
rollback->rollback contra el binario real). SDD 13 actualizado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 21:46:14 -04:00
sergioandClaude Opus 4.8 b9d3170859 hammer-mirror: mirror del store content-addressed (Etapa E3, primer corte)
Nuevo crate hammer-mirror + CLI `hammer mirror push|pull|status <remote>`. El /store es un
CAS (dir `<hash>-<name>`, el hash ES la dirección); un mirror = ese CAS replicado entre
máquinas + resolución por hash. La integridad se ancla en of_tree (content-hash BLAKE3 del
árbol): el receptor RECOMPUTA of_tree sobre lo copiado y exige que case el del índice ANTES
de sellar (Store::seal, atómico + read-only en un staging del mismo FS) ⇒ una transferencia
corrupta/manipulada se RECHAZA, no se instala.

- index(store): enumera dirs `<64hex>-<name>` (filtra bootstrap.json/.bootstrap-tmp), of_tree
  cada uno. diff(src,dst): faltantes en cada lado + conflictos (mismo dir, otro contenido).
- sync(src,dst): copia los que faltan (verificados), idempotente, no sobrescribe conflictos.
  push = sync(local,remoto); pull = sync(remoto,local).
- Transporte = filesystem (el remoto es una ruta a otro store, p.ej. sshfs); SSH/red queda
  como envoltorio ortogonal posterior. Endurecimiento: anclar of_tree a una raíz firmada
  (bootstrap.json / atestación D) vs un origen plenamente malicioso.

5 tests unitarios (forma de dir, índice, sync idempotente, rechazo de transferencia corrupta,
conflicto-no-sobrescribe) + validado E2E con el CLI sobre artefactos reales (push/pull/status,
idempotencia, bytes idénticos, tamper→conflicto, conflicto no sobrescrito exit≠0).

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:22:44 +00:00
Sergio 5986200539 Fase 6 — traductor LLM real (Claude API) detrás de feature llm-claude
`ClaudeTranslator` implementa el trait `IntentTranslator` igual que el
`MockTranslator`, así que el `Orchestrator` no cambia: se traduce una
intención NL a un `.swm` válido vía `/v1/messages`.

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

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

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

Tests (7 unit): happy path con verificación de URL/headers/body,
unwrap de markdown, `refusal`, error envelope, SWM mal formado,
verify_schema, helpers de strip_code_fence.
2026-06-10 16:29:27 +00:00
Sergio 89ecd54135 Fase 6 — bucle agéntico: hammer-agent (cliente + translator + orchestrator) y hammer ai
- proto: mover hammerd::proto a hammer-core::proto para que hammerd y hammer-agent
  compartan los tipos del bus sin duplicar.
- hammer-agent (crate nuevo):
  * client: AgentClient síncrono. Handshake hello/welcome; compile/inject/query/init
    bloqueantes con timeout; reader thread interno demultiplexa async events (Modified/
    Crashed) en una cola que el caller drena vía drain_async/next_async.
  * translator: trait IntentTranslator + MockTranslator (HashMap<intent, Swm>) +
    IntentCatalog YAML (swm_inline o swm_path). El traductor LLM real se enchufa
    detrás del mismo trait sin cambios al orquestador.
  * orchestrator: Orchestrator::run(intent) -> Proposal con plan -> schema -> base ->
    try (overlay|prefix) -> apply (config_edit/file_drop con hammer-core::apply,
    source_patch via bus opcional) -> verify (spot-checks) -> propose. Devuelve
    overlay_id (para `hammer commit`) o prefix usado.
- hammer-cli: subcomando `hammer ai <intent> --catalog F [--prefix DIR --base-ref F
  --bus SOCK --state-root DIR]`. Imprime el Proposal y el siguiente paso humano.
- Tests:
  * 10 unit (translator + catalog + orchestrator).
  * 3 e2e del bucle agéntico (intent -> archivos esperados bajo un prefix tmp).
  * 1 e2e del cliente contra un stub bus (handshake + Compile -> BuildReady +
    Modified asíncrono), sin depender de hammerd ni del lab.
- Docs: SDD 08 actualizado con el API del crate; roadmap marca lo cerrado y lo
  pendiente (LLM real, lenguaje de consulta, bucle de auto-reparación con Crashed).
2026-06-09 15:47:37 +00:00
Sergio 09d50f9e60 Fase 4 — formato .swm: verify, apply (config_edit/file_drop/source_patch) y export
- hammer-core::swm: verify_schema (invariantes por mutación) + verify_base con
  BaseRef/BaseCompat/PinDiff (distro_version + pins). FileDrop.content_b64 para
  .swm autocontenidos.
- hammer-core::apply: primitivas puras apply_config_edit (hunks -/+ con búsqueda
  exacta de bloque, ambiguo => error), apply_file_drop (base64 + verify BLAKE3),
  rebase_path para tests con prefix.
- hammer-build::swm_bridge: Mutation::SourcePatch -> Recipe sintética + patch
  inline materializado + build, con comparación opcional contra expected_hash.
- hammer-cli: nuevos subcomandos apply [--prefix --base-ref --skip-source-patch
  --state-root], swm-verify, export --journal --base-ref --since.
- Tests: 18 unit nuevos en hammer-core (apply + verify), 5 en swm_bridge,
  4 e2e en hammer-cli (roundtrip yaml -> apply bajo prefix reproduce los archivos).
- Roadmap y SDD 06 actualizados con lo cerrado y lo pendiente (URLs remotos,
  provenance en export vía mapa artefacto->receta, firma ed25519).
2026-06-09 15:19:33 +00:00
SergioandClaude Opus 4.7 a87b16dd85 Fase 3 — diario de mutaciones (hammer-journal + fanotify + hammer journal)
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>
2026-06-09 14:43:07 +00:00
SergioandClaude Opus 4.7 fe1601e669 Fase 2 — overlay try/commit/discard/status sobre overlayfs
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>
2026-06-09 14:26:17 +00:00
sergioandClaude Opus 4.8 8bf1623044 Scaffold inicial: workspace Rust + SDDs completos
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>
2026-06-06 19:06:04 +00:00