On February 14, 2026, at approximately 2:15 PM West Africa Time, Juste opened a terminal in Abidjan and said: "Build me a real-time code change tracker with AI agent detection. Rust. CLI. Ship today."
Forty-five minutes later, 0diff existed. Eight source files, 2,356 lines, 44 passing tests, 11 dependencies, a 2.0MB release binary, and five commands. Three weeks after that, launch prep took twenty minutes.
We are Juste (CEO, ZeroSuite) and Claude (AI CTO). This is the final article in our series about building 0diff. The previous three articles covered the why (agent attribution crisis), the file watching and diff engine, and the AI agent detection system. This one covers how the entire thing was built and shipped.
---
Session 314: The Build
The build happened in a single session. Not because we were in a rush, but because parallel agents make serial development obsolete for a tool of this scope.
The Team Structure
Five agents worked simultaneously, coordinated by a team lead:
| Agent | Module | Lines | Responsibility |
|---|---|---|---|
| agent-config | config.rs | 456 | TOML parsing, default values, should_watch with glob matching |
| agent-differ | differ.rs + filter.rs | 176 | Diff computation using the similar crate, grouped hunks with 3-line context, whitespace filtering |
| agent-git | git.rs + agents.rs | 311 | Shell-based git integration, Co-Authored-By parsing, 3-tier agent detection |
| agent-history | history.rs + output.rs | 760 | JSON-lines append-only history, rotation by age and size, colored terminal output + JSON format |
| agent-site | index.html | 1,625 | Marketing landing page with inline CSS |
| Team lead | Cargo.toml + main.rs + lib.rs + watcher.rs | -- | Project skeleton, dependency management, CLI definition, event loop |
The team lead -- Claude, operating as a coordinator -- defined the module boundaries and public interfaces first, then dispatched each agent with a clear scope. Each agent worked in isolation on its module, producing both the implementation and the tests. The team lead handled integration: the Cargo.toml, the lib.rs that exports all modules, the main.rs with the CLI definition, and the watcher.rs that ties everything together.
This is not a theoretical workflow. This is how we build at ZeroSuite. Juste defines the product. Claude (as CTO) decomposes it into modules. Parallel agents implement the modules. The team lead integrates. The result is a complete tool in under an hour.
The Config System
The config module deserves special attention because it demonstrates a principle we follow across all ZeroSuite tools: zero-config defaults with full customization.
#[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
}Five sections. Every field has a default. The #[serde(default)] attribute on each struct means that TOML parsing succeeds even with a completely empty config file:
#[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);
}This means 0diff init && 0diff watch works out of the box with sensible defaults: watch src/, app/, and entities/ directories; track .rs, .ts, .js, .py, .go, .java, and .flin files; ignore target/, node_modules/, and .git/; debounce at 500ms; detect all five major AI agents.
But if you need to customize -- different watch paths, different ignore patterns, custom agent detection strings -- you edit one section of the TOML and the rest keeps its defaults. Partial configs parse cleanly:
#[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);
}This is not clever engineering. It is Rust's serde and toml crates doing exactly what they were designed to do. The clever part is recognizing that a CLI tool that requires a 50-line config file before it does anything useful is a CLI tool nobody will use.
The History Format
agent-history chose JSON-lines (.jsonl) as the storage format. One JSON object per line, appended to .0diff/history.jsonl:
pub 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(())
}Why JSON-lines instead of SQLite, or a custom binary format, or even plain git notes?
Append-only is crash-safe. If 0diff crashes mid-write, the worst case is a truncated final line. The all_entries() method handles this gracefully by skipping invalid lines:
pub 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::
Ok(entries) } ```
That silent skip of invalid lines is not just crash recovery -- it is forward compatibility. If a future version of 0diff adds fields to HistoryEntry, old entries (missing those fields) still parse because serde uses Option for nullable fields. If a future version changes the format entirely, old entries get skipped instead of causing a panic.
JSON-lines is grep-friendly. You can grep "Claude" .0diff/history.jsonl and get useful results without any tooling. You can jq it. You can pipe it into other tools. This matters more than people think. When you are debugging at 3AM, the ability to cat your history file and read it with your eyes is worth more than a 10x query performance improvement.
Rotation keeps things bounded. The history store rotates on graceful exit (Ctrl+C), removing entries older than max_days and trimming by max_size_mb:
pub 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
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(()) } ```
Default limits: 10MB and 30 days. For a tool that writes maybe 200 bytes per file change event, 10MB is roughly 50,000 entries. That is months of heavy development without rotation ever kicking in.
The Output Module
Every user-facing output in 0diff goes through output.rs, which supports two formats: colored terminal and JSON. This dual-format approach means 0diff works equally well for human developers reading terminal output and for scripts, CI pipelines, and dashboards consuming structured data.
pub fn print_change_event(entry: &HistoryEntry, diff: &FileDiff, format: OutputFormat) {
match format {
OutputFormat::Terminal => {
let time_str = extract_time(&entry.timestamp);let agent_tag = if entry.agent.is_some() { format!(" {}", "[AI AGENT]".yellow().bold()) } else { String::new() };
println!( "{} {} {} {} {}{}", format!("[{}]", time_str).dimmed(), entry.file.bold().white(), format!("+{}", entry.additions).green(), format!("-{}", entry.deletions).red(), author_info, agent_tag, ); // ... diff hunks follow } OutputFormat::Json => { let event = ChangeEvent { entry, diff }; if let Ok(json) = serde_json::to_string_pretty(&event) { println!("{}", json); } } } } ```
The [AI AGENT] tag in yellow bold is the most visible signal in the terminal output. When you are watching a stream of file changes and one of them was made by an AI agent, it stands out immediately. No need to read the details -- the yellow tag catches your eye.
The --json flag is global across all commands. 0diff log --json, 0diff status --json, 0diff diff file.rs --json -- they all produce structured JSON. This was not an afterthought. It was a requirement from the start, because 0diff needs to integrate into pipelines and dashboards, not just terminals.
---
The Seven Bugs
No session is bug-free. Here are the seven issues we hit and fixed during the build:
1. DebouncedEventKind::AnySynthetic does not exist. The notify-debouncer-mini crate's API changed between versions. The correct variant is DebouncedEventKind::Any. The agent-watcher initially used the wrong variant name. Fixed by checking the crate documentation.
2. notify::Error is not iterable. An early version of the watcher tried to iterate over notify::Error as if it were a collection of errors. It is a single error type. Fixed by matching on Ok(Err(error)) directly.
3. Glob ignore pattern matching was not recursive. The first implementation of should_watch checked ignore patterns only against the full path. A pattern like target/ would match target/debug/build.rs but not src/target/debug.rs. Fixed by also matching against individual path components:
``rust
let clean = pattern_str.trim_end_matches('/');
if let Ok(pattern) = glob::Pattern::new(clean) {
for component in path.components() {
let comp = component.as_os_str().to_string_lossy();
if pattern.matches(&comp) {
return false;
}
}
}
``
4. Agent file clobbering. Two agents initially wrote to the same file. The team lead caught this during integration and reassigned one agent's output to a different file. This is a coordination problem unique to parallel agent development -- and it is the team lead's primary job to prevent it.
5. GitHub org links. The marketing site initially linked to the wrong GitHub organization. Fixed during review.
6. Cargo.toml repository URL. The repository field initially pointed to a placeholder URL. Updated to https://github.com/zerosuite-inc/0diff.
7. install.sh TMPDIR conflict. The install script used TMPDIR as a variable name, which conflicts with the macOS system variable of the same name. Renamed to TMPD. This is the kind of bug you only discover on macOS, and we develop on macOS.
Seven bugs in 45 minutes of parallel development. All caught before the session ended. All fixed in minutes. The overhead of bug-fixing was less than the time saved by parallelism.
---
Session 315: Launch Prep (20 Minutes)
Three weeks later, on March 9, 2026, we did the launch prep. Twenty minutes.
Replaced all 25 emojis in the marketing site with inline Lucide SVGs. ZeroSuite has a strict no-emoji policy in all user-facing content. The original marketing page, written quickly during the build session, used emojis as visual markers. Each was replaced with a properly sized, themed SVG icon.
Added target="_blank" to all 10 external links. A marketing page should not navigate away from itself when users click an external link. Every link to GitHub, the documentation, and the install script now opens in a new tab.
Fixed install.sh TMPDIR conflict. This was bug number 7 from the build session, but it required testing on a clean macOS environment to verify the fix. Confirmed working.
Enhanced Dockerfile. Added a custom nginx.conf for proper MIME types. The default nginx configuration does not serve .wasm files with the correct Content-Type, and while 0diff itself is not a web application, the marketing site needed correct static asset serving.
Final verification: cargo test -- 44 tests, all passing. Zero warnings. Clean build.
That is the entire launch prep. No feature changes. No architecture revisions. No "oh wait, we forgot to handle this edge case." The build session produced a complete, tested, working tool. The launch prep was polish.
---
Final Stats
| Metric | Value |
|---|---|
| Source files | 8 .rs files |
| Total lines | ~2,356 |
| Tests | 44 (all passing) |
| Dependencies | 11 |
| Release binary | 2.0 MB |
| Commands | 5 (init, watch, diff, log, status) |
| Build session | ~45 minutes (Session 314) |
| Launch prep | ~20 minutes (Session 315) |
| Total human time | ~65 minutes across both sessions |
Eleven dependencies, no more:
[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"Each dependency does one thing. notify for filesystem events. similar for diff computation. clap for CLI parsing. toml and serde for config. colored for terminal output. chrono for timestamps. glob for pattern matching. ctrlc for graceful shutdown. No frameworks. No runtime. No async.
---
What Makes 0diff Different
Every conversation about 0diff eventually produces the question: "Why not just use git diff?" or "Why not watchexec?" Here is the honest comparison:
| Feature | git diff | watchexec | fswatch | 0diff |
|---|---|---|---|---|
| Real-time watching | No | Yes | Yes | Yes |
| Diff computation | Committed only | No | No | Yes (live) |
| AI agent detection | No | No | No | Yes |
| History tracking | Via commits | No | No | Yes (JSON-lines) |
| Git metadata | N/A | No | No | Yes (author, branch) |
| Whitespace filter | Partial | N/A | N/A | Yes |
| JSON output | Patch format | No | No | Yes |
| Single binary | N/A | Yes | Yes | Yes |
| Config file | .gitconfig | CLI args | CLI args | .0diff.toml |
git diff only shows you what has changed relative to the last commit. It does not watch. It does not detect agents. It does not maintain history beyond git's own commit log.
watchexec and fswatch watch for file changes, but they only tell you that a file changed -- not what changed, not who changed it, and not whether an AI agent was involved.
0diff sits in a unique position: it combines file watching, diff computation, git metadata enrichment, and AI agent detection in a single tool. None of the alternatives do this. They were not designed to, because the problem 0diff solves -- multi-agent code attribution -- did not exist when they were built.
---
How 0diff Fits Into ZeroSuite
There is a recursive irony to 0diff: it was built by the very AI agents it is designed to track.
Session 314 -- the session that built 0diff -- had five AI agents modifying code in parallel. If 0diff had existed at the time, it would have detected each of those agents via their Co-Authored-By trailers and environment variables. It would have recorded every file modification with timestamps, diff sizes, and agent tags. It would have produced a complete audit trail of its own creation.
This is not an accident. ZeroSuite builds tools that solve problems we experience firsthand. We run Claude Code across multiple parallel agents for nearly every product we build. The attribution gap is something we encounter every day. 0diff is not a speculative product for a hypothetical future -- it is a tool we needed, built by the workflow it was designed to monitor.
0diff joins the ZeroSuite product line alongside sh0.dev (the shell), 0cron.dev (the cron manager), 0seat.dev (event ticketing), Flin (the programming language), and Deblo.ai (the educational platform). Each product was built with the same methodology: Juste defines, Claude decomposes, agents implement, the team lead integrates. And increasingly, 0diff watches the whole process.
---
What We Learned
Parallel agents work for decomposable problems. 0diff has clean module boundaries: config, diff, git, history, output, watcher. Each module has a well-defined interface. This makes it ideal for parallel development. A monolithic state machine with complex inter-module dependencies would not decompose as cleanly.
The team lead is the bottleneck, and that is correct. The five agents can produce code faster than a single coordinator can integrate it. But the coordinator's job -- defining interfaces, resolving conflicts, maintaining architectural coherence -- is the hard part. Making the integration bottleneck explicit is better than pretending it does not exist.
Test coverage is not optional in parallel development. When five agents produce code independently, the only way to verify correctness at integration time is to run the tests. All 44 tests passing after integration is not just a quality signal -- it is the proof that the module boundaries were drawn correctly.
Ship, then polish. Session 314 produced a working tool with rough edges (emojis in the marketing page, a TMPDIR conflict on macOS). Session 315 polished those edges in 20 minutes. The temptation to polish during the build session is strong, but it breaks the parallel workflow. Agents should produce correct, tested code. Polish is a serial activity.
Rust is the right language for CLI tools. A 2.0MB binary with zero runtime dependencies, instant startup, cross-platform compilation, and a type system that catches integration errors at compile time. The initial learning curve is steep, but for a tool that needs to be fast, small, and reliable, nothing else comes close.
---
The Series
This is the final article in the "How We Built 0diff" series. If you have read all four, you now know everything about 0diff: why it exists, how the file watcher and diff engine work, how agent detection operates, and how the whole thing was built and shipped.
0diff is open source at github.com/zerosuite-inc/0diff. Install it and run 0diff init && 0diff watch. Then open another terminal and let Claude Code make some changes. Watch the [AI AGENT] tags appear.
Welcome to the multi-agent era. Know who changed what.
---
This is Part 4 of the "How We Built 0diff" series:
1. Why We Built a Code Change Tracker for the AI Agent Era 2. Real-Time File Watching and Diff Computation in Rust 3. Detecting AI Agents in Your Codebase 4. From 5 Agents to Production: Shipping 0diff in 20 Minutes (you are here)