enjambre · agentes de código repartidos entre dos máquinas · python + bash

Arquitectura de enjambre

Un PC con Windows manda tareas a una Mac y recoge ramas de git. El PC valida, enruta, vigila y publica; la Mac ejecuta con un agente de código, aislada en un worktree por trabajo. Cinco vistas: contenedores, datos, flujos con el ciclo de vida de un trabajo, despliegue y decisiones. Haz clic en una caja; reproduce un flujo para ver el recorrido.

propio (este repo) existe fuera del repo servicio externo tráfico
Detalle

Haz clic en un contenedor

Cada caja es una pieza que corre por separado: un script, un servicio, un repo o un sitio. Las líneas son llamadas reales: protocolo y qué viaja. La caja «ssh» agrupa la única puerta del PC hacia la Mac.

Por qué git es el bus

El resultado de un trabajo no es un archivo suelto sino una rama. La Mac la deja en su clon, Windows la trae por ssh, la escanea con gitleaks y recién entonces la sube. Revisar y descartar son operaciones de git, y fusionar sigue siendo una decisión de una persona.

Por qué un supervisor aparte

Un ssh que espera pierde el resultado si se cierra la ventana. El supervisor del nodo vive más que la conexión: lanza el trabajo en su propio grupo de procesos, late, escribe el estado final y deja el commit. Windows solo recoge cuando le conviene.

Por qué un solo canal ssh

Todo lo que va a la Mac pasa por una conexión autenticada y cifrada, sin abrir ningún puerto de red nuevo. Hasta el puente conversacional escucha solo en loopback y se alcanza con curl dentro de ese mismo ssh, además de una firma HMAC por petición.

Almacenamiento · archivos JSON y una base SQLite · pasa el cursor por una relación

Qué es cada almacén

jobs.jsonl
El ledger, en el PC: una línea por trabajo, solo se añade. Sin línea no pasó (enjambre.py: append_ledger). Los latidos nunca van aquí (DECISIONES 202).
job.json
En el PC, state/jobs/{id}/: qué se lanzó, dónde y si ya se recogió (largos.py: write_meta).
spec, status, latidos
En la Mac, ~/.enjambre/jobs/{id}/: lo que el PC pide, lo que el supervisor reescribe en cada latido y una línea por intervalo (jobrun.py).
result.json
Solo el run síncrono: lo escribe worker.sh en el worktree y viaja en el commit de la rama.
history.db
SQLite del visor, una por nodo y sin podar: la limpieza de transcripciones de Claude Code no borra el historial (tools/visor/collect.py: HISTORY_SCHEMA).

Las relaciones son implícitas

Ni los archivos JSON ni history.db declaran claves foráneas. Los vínculos se resuelven por convención: el mismo id de trabajo en spec, status y job; el path de la transcripción entre files, msgs y agents (collect.py: db_load); y origin_session del ledger contra el id de sesión, con que el visor marca qué sesiones mandaron trabajos a otro nodo (build.py: remote_jobs_by_session).

El ledger tiene unos 40 campos posibles según quién escribe la línea (run síncrono, trabajo largo o turno del puente). La tarjeta muestra el núcleo común; el resto está en la nota de la tabla y en docs/arquitectura.notas.md.
Secuencias · una por flujo · los mismos pasos que reproduce el C4
Ciclo de vida de un trabajo largo · estados reales del código
Despliegue · lo que hay y lo que cambiaría

Hoy un PC, una Mac y dos sitios estáticos o ligeros. Más nodos con una orden, sin tocar el código.

No hay servidor central ni base de datos que operar: el PC corre tres procesos residentes y la Mac los suyos como servicios de usuario. Lo único que vive en internet son dos sitios de Hostinger, y ambos solo reciben datos ya filtrados.

Hoy · PC Windows, Mac mini y Hostinger

watch al iniciar sesión (tarea programada)tools/install-watch-task.ps1
live --serve al iniciar sesióntools/install-live-task.ps1
build del visor cada 5 mintools/visor/install-visor-task.ps1
servidor tmux al abrir la sesión gráficadev.enjambre.tmux
colector del visor cada 5 mindev.enjambre.visor
puente conversacional, siempre vivodev.enjambre.puente
vigilancia del orquestador y montajes SMB, cada 60 sdev.enjambre.nodewatch · smbmount
demo estática en su subdominio, carpeta enjambre-appHostinger

Requisitos en la Mac: bash 3.2 (el de macOS), python 3 del sistema para el supervisor y tmux. Los trabajos largos y el puente necesitan la sesión gráfica abierta al menos una vez para que el llavero esté desbloqueado.

→ misma receta →

Al escalar · más nodos

enjambre nodo add: mide, copia el colector, instala el lanzador y espera el primer snapshotnodo.py
cupo por nodo y por prioridadnodes.toml · config/scheduler.toml
el enrutador elige por capacidades (tags) y por tiempos medidosrouting.toml
nodo Linux: falta el temporizador de systemd-usernodo add lo rechaza
LaunchAgent de tmux y del puente en el nodo nuevoa mano, una vez

Qué gana: otro nodo entra al reparto y al visor con una orden. Qué no gana: automatizar el alta de tmux y del puente, ni nodos Linux.

Decisiones · del registro de decisiones del proyecto, con la alternativa descartada

Cada decisión dice qué se eligió, qué se dejó y por qué

N.ºDecisiónPor quéDescartado
27El tmux del nodo nace del arranque gráfico y nunca se crea por sshEl llavero de macOS solo está desbloqueado en la sesión gráfica; por ssh, claude y git aparecen sin sesiónCrear la sesión por ssh: nacería sin llavero. tmux-run.sh sale con 75 y avisa cómo arreglarlo
29El nodo no hace push; Windows trae la rama, la escanea y la subeUna sola puerta de control, en la máquina que tiene gitleaks, sobre exactamente los commits nuevosPush desde el nodo, que no pasa por el escáner. Solo existe con ENJAMBRE_NODE_PUSH=1
209El push es opt-in en repos públicosUna rama en un remoto público queda publicada al llegar, haya o no escaneoSubir siempre. Sin «push:» en la TASK, solo los repos privados suben
200Un supervisor en el nodo, no un ssh que esperaWindows vuelve en segundos y el resultado no depende de que la ventana siga abiertaAlargar tmux-run.sh: espera con tope de 7200 s y devuelve al final; cerrar la ventana perdía el ledger
202Los latidos no van al ledgerEl ledger mantiene «una línea por trabajo» y cuatro lectores dependen de elloLíneas «event: heartbeat» en jobs.jsonl: obligaba a cambiar a los cuatro lectores
280La pausa la aplica el supervisor del propio nodostatus.json conserva un único escritor; antes de cualquier TERM va un SIGCONT porque un proceso detenido no muere limpiossh kill -STOP directo desde el PC: dos escritores del estado
237El puente se alcanza por ssh y curl a loopback, con firma HMACssh ya autentica; la firma cubre marca de tiempo, nonce, método, ruta y hash del cuerpo, y rechaza peticiones con OriginEscuchar en la LAN con un token: un puerto más expuesto a la red
247El puente no edita por defectoTrabaja en el clon principal, sin rama; una copia sucia rompe git pull --ff-only y los worktrees de los workersPermitir ediciones siempre. Los cambios de código siguen por enjambre run (rama y revisión); --edit es por turno
174La web recibe la instantánea por POST firmadoNo hay que guardar una credencial de hosting en el PC; la casa solo sale, nada entraSFTP con la contraseña del hosting: da acceso a todo el sitio