El 14 de febrero de 2026, aproximadamente a las 2:15 PM hora de Africa Occidental, Juste abrio una terminal en Abidjan y dijo: "Construyeme un rastreador de cambios de codigo en tiempo real con deteccion de agentes de IA. Rust. CLI. Se despliega hoy."
Cuarenta y cinco minutos despues, 0diff existia. Ocho archivos fuente, 2,356 lineas, 44 tests pasando, 11 dependencias, un binario de release de 2.0MB y cinco comandos. Tres semanas despues, la preparacion de lanzamiento tomo veinte minutos.
Somos Juste (CEO, ZeroSuite) y Claude (CTO de IA). Este es el articulo final de nuestra serie sobre la construccion de 0diff. Los tres articulos anteriores cubrieron el por que (crisis de atribucion de agentes), el motor de vigilancia de archivos y diff, y el sistema de deteccion de agentes de IA. Este cubre como se construyo y desplego todo.
Sesion 314: La construccion
La construccion ocurrio en una sola sesion. No porque tuvieramos prisa, sino porque los agentes en paralelo hacen obsoleto el desarrollo secuencial para una herramienta de este alcance.
La estructura del equipo
Cinco agentes trabajaron simultaneamente, coordinados por un lider de equipo:
| Agente | Modulo | Lineas | Responsabilidad |
|---|---|---|---|
| agent-config | config.rs | 456 | Parseo TOML, valores predeterminados, should_watch con coincidencia glob |
| agent-differ | differ.rs + filter.rs | 176 | Calculo de diff usando la crate similar, hunks agrupados con contexto de 3 lineas, filtrado de espacios en blanco |
| agent-git | git.rs + agents.rs | 311 | Integracion git basada en shell, parseo de Co-Authored-By, deteccion de agentes de 3 niveles |
| agent-history | history.rs + output.rs | 760 | Historial de solo adicion en JSON-lines, rotacion por edad y tamano, salida coloreada de terminal + formato JSON |
| agent-site | index.html | 1,625 | Pagina de marketing con CSS inline |
| Lider de equipo | Cargo.toml + main.rs + lib.rs + watcher.rs | -- | Esqueleto del proyecto, gestion de dependencias, definicion de CLI, bucle de eventos |
El lider de equipo -- Claude, operando como coordinador -- definio los limites de modulos e interfaces publicas primero, luego despacho a cada agente con un alcance claro. Cada agente trabajo en aislamiento en su modulo, produciendo tanto la implementacion como los tests. El lider de equipo manejo la integracion: el Cargo.toml, el lib.rs que exporta todos los modulos, el main.rs con la definicion de CLI, y el watcher.rs que une todo.
Este no es un flujo de trabajo teorico. Asi es como construimos en ZeroSuite. Juste define el producto. Claude (como CTO) lo descompone en modulos. Agentes en paralelo implementan los modulos. El lider de equipo integra. El resultado es una herramienta completa en menos de una hora.
El sistema de configuracion
El modulo de configuracion merece atencion especial porque demuestra un principio que seguimos en todas las herramientas de ZeroSuite: configuracion cero por defecto con personalizacion completa.
rust#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct Config {
pub watch: WatchConfig, // paths, ignore, extensions, debounce
pub filter: FilterConfig, // ignore_whitespace, min_lines_changed
pub git: GitConfig, // enabled, track_author, track_branch
pub history: HistoryConfig, // max_size_mb, max_days
pub agents: AgentConfig, // detect_patterns, tag_non_human
}Cinco secciones. Cada campo tiene un valor predeterminado. El atributo #[serde(default)] en cada estructura significa que el parseo TOML tiene exito incluso con un archivo de configuracion completamente vacio:
rust#[test]
fn test_toml_empty_config() {
let config: Config = toml::from_str("").expect("empty config should parse");
assert_eq!(config.watch.debounce_ms, 500);
assert!(config.filter.ignore_whitespace);
}Esto significa que 0diff init && 0diff watch funciona de inmediato con valores predeterminados sensatos: vigilar los directorios src/, app/, y entities/; rastrear archivos .rs, .ts, .js, .py, .go, .java, y .flin; ignorar target/, node_modules/, y .git/; debounce a 500ms; detectar los cinco agentes principales de IA.
Pero si necesitas personalizar -- diferentes rutas de vigilancia, diferentes patrones de ignorar, cadenas personalizadas de deteccion de agentes -- editas una seccion del TOML y el resto mantiene sus valores predeterminados. Las configuraciones parciales se parsean limpiamente:
rust#[test]
fn test_toml_partial_config() {
let partial = r#"
[watch]
debounce_ms = 1000
"#;
let config: Config = toml::from_str(partial).expect("partial config should parse");
assert_eq!(config.watch.debounce_ms, 1000);
assert_eq!(config.watch.paths, WatchConfig::default_paths());
assert!(config.git.enabled);
assert_eq!(config.history.max_days, 30);
}Esto no es ingenieria ingeniosa. Es que las crates serde y toml de Rust hacen exactamente lo que fueron disenadas para hacer. La parte ingeniosa es reconocer que una herramienta CLI que requiere un archivo de configuracion de 50 lineas antes de hacer algo util es una herramienta CLI que nadie va a usar.
El formato de historial
agent-history eligio JSON-lines (.jsonl) como formato de almacenamiento. Un objeto JSON por linea, anadido a .0diff/history.jsonl:
rustpub fn append(&self, entry: &HistoryEntry) -> Result<(), Box<dyn std::error::Error>> {
let path = self.history_path();
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let json = serde_json::to_string(entry)?;
writeln!(file, "{}", json)?;
Ok(())
}Por que JSON-lines en lugar de SQLite, o un formato binario personalizado, o incluso notas de git?
De solo adicion es seguro ante caidas. Si 0diff se cae a mitad de escritura, el peor caso es una ultima linea truncada. El metodo all_entries() maneja esto graciosamente saltando lineas invalidas:
rustpub fn all_entries(&self) -> Result<Vec<HistoryEntry>, Box<dyn std::error::Error>> {
let path = self.history_path();
if !path.exists() {
return Ok(Vec::new());
}
let file = fs::File::open(path)?;
let reader = BufReader::new(file);
let mut entries = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(entry) = serde_json::from_str::<HistoryEntry>(trimmed) {
entries.push(entry);
}
// Skip invalid lines gracefully
}
Ok(entries)
}Ese salto silencioso de lineas invalidas no es solo recuperacion ante caidas -- es compatibilidad hacia adelante. Si una version futura de 0diff anade campos a HistoryEntry, las entradas antiguas (sin esos campos) aun se parsean porque serde usa Option para campos anulables. Si una version futura cambia el formato completamente, las entradas antiguas se saltan en lugar de causar un panic.
JSON-lines es amigable con grep. Puedes grep "Claude" .0diff/history.jsonl y obtener resultados utiles sin ninguna herramienta. Puedes usar jq. Puedes canalizarlo a otras herramientas. Esto importa mas de lo que la gente piensa. Cuando estas depurando a las 3 AM, la capacidad de hacer cat a tu archivo de historial y leerlo con tus ojos vale mas que una mejora de rendimiento de consulta de 10x.
La rotacion mantiene las cosas acotadas. El almacen de historial rota en salida gracil (Ctrl+C), eliminando entradas mas antiguas que max_days y recortando por max_size_mb:
rustpub fn rotate(
&self,
max_size_mb: u64,
max_days: u64,
) -> Result<(), Box<dyn std::error::Error>> {
let entries = self.all_entries()?;
let cutoff = Utc::now() - chrono::Duration::days(max_days as i64);
let mut kept: Vec<HistoryEntry> = entries
.into_iter()
.filter(|e| {
DateTime::parse_from_rfc3339(&e.timestamp)
.map(|dt| dt.with_timezone(&Utc) >= cutoff)
.unwrap_or(true) // keep entries with unparseable timestamps
})
.collect();
let max_bytes = max_size_mb * 1024 * 1024;
loop {
let size: usize = kept
.iter()
.map(|e| serde_json::to_string(e).unwrap_or_default().len() + 1)
.sum();
if (size as u64) <= max_bytes || kept.is_empty() {
break;
}
kept.remove(0); // Remove oldest entry
}
// Rewrite the file
let path = self.history_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
for entry in &kept {
let json = serde_json::to_string(entry)?;
writeln!(file, "{}", json)?;
}
Ok(())
}Limites predeterminados: 10MB y 30 dias. Para una herramienta que escribe quiza 200 bytes por evento de cambio de archivo, 10MB es aproximadamente 50,000 entradas. Eso son meses de desarrollo intenso sin que la rotacion se active jamas.
Los siete bugs
Ninguna sesion esta libre de bugs. Aqui estan los siete problemas que encontramos y corregimos durante la construccion:
DebouncedEventKind::AnySyntheticno existe. La API de la cratenotify-debouncer-minicambio entre versiones. La variante correcta esDebouncedEventKind::Any. El agent-watcher inicialmente uso el nombre de variante incorrecto. Se corrigio revisando la documentacion de la crate.
notify::Errorno es iterable. Una version temprana del observador intento iterar sobrenotify::Errorcomo si fuera una coleccion de errores. Es un solo tipo de error. Se corrigio coincidiendo enOk(Err(error))directamente.
- La coincidencia de patrones de ignorar glob no era recursiva. La primera implementacion de
should_watchverificaba patrones de ignorar solo contra la ruta completa. Un patron comotarget/coincidiria contarget/debug/build.rspero no consrc/target/debug.rs. Se corrigio coincidiendo tambien contra componentes individuales de la ruta.
- Sobrescritura de archivos entre agentes. Dos agentes inicialmente escribieron al mismo archivo. El lider de equipo detecto esto durante la integracion y reasigno la salida de un agente a un archivo diferente. Este es un problema de coordinacion unico del desarrollo con agentes en paralelo -- y es el trabajo principal del lider de equipo prevenirlo.
- Enlaces de organizacion GitHub. El sitio de marketing inicialmente enlazaba a la organizacion GitHub incorrecta. Se corrigio durante la revision.
- URL del repositorio en Cargo.toml. El campo de repositorio inicialmente apuntaba a una URL placeholder. Se actualizo a
https://github.com/zerosuite-inc/0diff.
- Conflicto de TMPDIR en
install.sh. El script de instalacion usabaTMPDIRcomo nombre de variable, lo que entra en conflicto con la variable del sistema macOS del mismo nombre. Se renombro aTMPD. Este es el tipo de bug que solo descubres en macOS, y desarrollamos en macOS.
Siete bugs en 45 minutos de desarrollo en paralelo. Todos detectados antes de que terminara la sesion. Todos corregidos en minutos. La sobrecarga de correccion de bugs fue menor que el tiempo ahorrado por el paralelismo.
Sesion 315: Preparacion de lanzamiento (20 minutos)
Tres semanas despues, el 9 de marzo de 2026, hicimos la preparacion de lanzamiento. Veinte minutos.
Reemplazamos los 25 emojis del sitio de marketing con SVGs inline de Lucide. ZeroSuite tiene una politica estricta de cero emojis en todo contenido visible al usuario. La pagina de marketing original, escrita rapidamente durante la sesion de construccion, usaba emojis como marcadores visuales. Cada uno fue reemplazado con un icono SVG correctamente dimensionado y tematizado.
Anadimos target="_blank" a los 10 enlaces externos. Una pagina de marketing no deberia navegar fuera de si misma cuando los usuarios hacen clic en un enlace externo. Cada enlace a GitHub, la documentacion y el script de instalacion ahora se abre en una nueva pestana.
Corregimos el conflicto de TMPDIR en install.sh. Este era el bug numero 7 de la sesion de construccion, pero requeria pruebas en un entorno macOS limpio para verificar la correccion. Confirmado funcionando.
Mejoramos el Dockerfile. Anadimos un nginx.conf personalizado para tipos MIME correctos. La configuracion predeterminada de nginx no sirve archivos .wasm con el Content-Type correcto, y aunque 0diff en si no es una aplicacion web, el sitio de marketing necesitaba servir activos estaticos correctamente.
Verificacion final: cargo test -- 44 tests, todos pasando. Cero advertencias. Compilacion limpia.
Esa es toda la preparacion de lanzamiento. Sin cambios de funcionalidad. Sin revisiones de arquitectura. Sin "oh espera, olvidamos manejar este caso borde." La sesion de construccion produjo una herramienta completa, probada y funcional. La preparacion de lanzamiento fue pulido.
Estadisticas finales
| Metrica | Valor |
|---|---|
| Archivos fuente | 8 archivos .rs |
| Total de lineas | ~2,356 |
| Tests | 44 (todos pasando) |
| Dependencias | 11 |
| Binario de release | 2.0 MB |
| Comandos | 5 (init, watch, diff, log, status) |
| Sesion de construccion | ~45 minutos (Sesion 314) |
| Preparacion de lanzamiento | ~20 minutos (Sesion 315) |
| Tiempo humano total | ~65 minutos en ambas sesiones |
Once dependencias, ni mas:
toml[dependencies]
notify = "8"
notify-debouncer-mini = "0.6"
similar = "2"
clap = { version = "4", features = ["derive"] }
toml = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
colored = "2"
chrono = { version = "0.4", features = ["serde"] }
glob = "0.3"
ctrlc = "3"Cada dependencia hace una cosa. notify para eventos del sistema de archivos. similar para calculo de diff. clap para parseo de CLI. toml y serde para configuracion. colored para salida de terminal. chrono para marcas de tiempo. glob para coincidencia de patrones. ctrlc para apagado gracil. Sin frameworks. Sin runtime. Sin asincronia.
Que hace diferente a 0diff
Cada conversacion sobre 0diff eventualmente produce la pregunta: "Por que no solo usar git diff?" o "Por que no watchexec?" Aqui esta la comparacion honesta:
| Funcionalidad | git diff | watchexec | fswatch | 0diff |
|---|---|---|---|---|
| Vigilancia en tiempo real | No | Si | Si | Si |
| Calculo de diff | Solo comprometido | No | No | Si (en vivo) |
| Deteccion de agentes de IA | No | No | No | Si |
| Rastreo de historial | Via commits | No | No | Si (JSON-lines) |
| Metadatos de git | N/A | No | No | Si (autor, rama) |
| Filtro de espacios en blanco | Parcial | N/A | N/A | Si |
| Salida JSON | Formato patch | No | No | Si |
| Binario unico | N/A | Si | Si | Si |
| Archivo de configuracion | .gitconfig | Args CLI | Args CLI | .0diff.toml |
git diff solo te muestra lo que ha cambiado en relacion al ultimo commit. No vigila. No detecta agentes. No mantiene historial mas alla del propio log de commits de git.
watchexec y fswatch vigilan cambios en archivos, pero solo te dicen que un archivo cambio -- no que cambio, quien lo cambio, ni si un agente de IA estuvo involucrado.
0diff ocupa una posicion unica: combina vigilancia de archivos, calculo de diff, enriquecimiento con metadatos de git y deteccion de agentes de IA en una sola herramienta. Ninguna de las alternativas hace esto. No fueron disenadas para ello, porque el problema que 0diff resuelve -- atribucion de codigo multi-agente -- no existia cuando fueron construidas.
Lo que aprendimos
Los agentes en paralelo funcionan para problemas descomponibles. 0diff tiene limites de modulos limpios: config, diff, git, historial, salida, observador. Cada modulo tiene una interfaz bien definida. Esto lo hace ideal para desarrollo en paralelo. Una maquina de estado monolitica con dependencias complejas entre modulos no se descompondria tan limpiamente.
El lider de equipo es el cuello de botella, y eso es correcto. Los cinco agentes pueden producir codigo mas rapido de lo que un solo coordinador puede integrarlo. Pero el trabajo del coordinador -- definir interfaces, resolver conflictos, mantener coherencia arquitectonica -- es la parte dificil. Hacer explicito el cuello de botella de integracion es mejor que pretender que no existe.
La cobertura de tests no es opcional en desarrollo en paralelo. Cuando cinco agentes producen codigo independientemente, la unica forma de verificar la correccion en el momento de la integracion es ejecutar los tests. Que los 44 tests pasen despues de la integracion no es solo una senal de calidad -- es la prueba de que los limites de modulos fueron trazados correctamente.
Desplegar, luego pulir. La Sesion 314 produjo una herramienta funcional con bordes asperos (emojis en la pagina de marketing, un conflicto de TMPDIR en macOS). La Sesion 315 pulio esos bordes en 20 minutos. La tentacion de pulir durante la sesion de construccion es fuerte, pero rompe el flujo de trabajo en paralelo. Los agentes deben producir codigo correcto y probado. El pulido es una actividad secuencial.
Rust es el lenguaje correcto para herramientas CLI. Un binario de 2.0MB con cero dependencias en tiempo de ejecucion, inicio instantaneo, compilacion multiplataforma, y un sistema de tipos que detecta errores de integracion en tiempo de compilacion. La curva de aprendizaje inicial es empinada, pero para una herramienta que necesita ser rapida, pequena y confiable, nada mas se acerca.
La serie
Este es el articulo final de la serie "Como construimos 0diff". Si has leido los cuatro, ahora sabes todo sobre 0diff: por que existe, como funcionan el observador de archivos y el motor de diff, como opera la deteccion de agentes, y como se construyo y desplego todo.
0diff es de codigo abierto en github.com/zerosuite-inc/0diff. Instalalo y ejecuta 0diff init && 0diff watch. Luego abre otra terminal y deja que Claude Code haga algunos cambios. Observa las etiquetas [AI AGENT] aparecer.
Bienvenido a la era multi-agente. Sabe quien cambio que.
Esta es la Parte 4 de la serie "Como construimos 0diff":
- Por que construimos un rastreador de cambios de codigo para la era de los agentes de IA
- Vigilancia de archivos en tiempo real y calculo de diff en Rust
- Deteccion de agentes de IA en tu codebase
- De 5 agentes a produccion: Desplegando 0diff en 20 minutos (estas aqui)