01 / Title
Deck 08 · the build story

How branchdiff grew up.

From one command that fixed a lie git diff was telling, to a self-hosted bot that reviews pull requests across a whole directory of repos while you sleep. This is the build story — why each piece exists, not a feature tour.

Apr → Sep 2026 25 releases 1 command → 1 bot GitHub + Bitbucket
02 · the origin

git diff compares commit paths. branchdiff compares content.

Two branches can hold identical bytes and still produce a phantom diff — because Git's two-dot diff compares commit history, not file content. A rebase, a cherry-pick, a squash: Git reports a "change" where there is none. That phantom diff is the whole reason branchdiff exists.

git diff compares commit paths; branchdiff compares content
Same SHA-1 blob hash on both tips → no diff. Comparing maps of hashes is O(n) on files, not on history.
03 · day one — a proof of concept

Deliberately small. Four flags.

The default compared actual content — the thing Git's own diff gets wrong. Tab-completion came free from the built-in readline; the whole thing was one local page reading the diff from disk.

terminal
$ branchdiff # interactive prompt, tab-completion $ branchdiff feat # current branch vs feat $ branchdiff main feat # two branches $ branchdiff main feat --mode git # commit-history diff
A week later it was on npm as v1.0.0, renamed @encryptioner/branchdiff, licensed Commons Clause + MIT — open to use, not open to resell as a hosted product.
04 · every feature was a personal itch

No roadmap. Each version was a toe I kept stubbing.

The release notes read less like marketing and more like a diary of things that irritated me. Each feature starts with "I keep having to…"

05 · the itch that became version 2

The unit of work is "my open PRs", not "this checkout's PRs".

I keep about ten side projects going at once. Each has its own repo, each occasionally has an open PR. Pointing auto at a directory replaced an entire morning of context-switching:

one line, ten repos
$ cd ~/work && branchdiff auto --tool claude
What it does: finds every open PR in every repo underneath, reviews only the ones with new commits since last time, and streams each session's URL as it goes. One numbered candidate list instead of ten terminals to reconcile by hand.
06 · auto, shaped by more toe-stubs

Each flag is a sentence that ends with "…so I added a flag".

  • The 900-file lockfile bump wastes a pass. So does a one-word typo fix. Size is the cheapest signal — --max-files / --max-lines skip the giants and leave them to a human.
  • I never want to review my own PR by accident. --skip-author.
  • A foreground terminal only reviews while it stays open. No good for a headless box or a fixed daily window — so auto learned to detach and to schedule itself.
  • I had no idea how much the tool had actually done for me. No telemetry, no summary. So stats was born — a dashboard over every local review database.
unattended
$ branchdiff auto --detach \ --review --tool claude \ --repo-paths ~/work --yes $ branchdiff auto cron add \ --start "0 9 * * 1-5" \ --review --repo-paths ~/work
07 · the company angle — review at scale

The same command that reviews one PR reviews a hundred.

The only difference is which directory you point it at. That is what makes it a self-hosted review bot a team can actually run.

N repos
Multi-repo scan
One directory → every open PR found, reviewed, deduped.
cron
Runs unattended
Detach to the background, or schedule a fixed daily window.
2 forges
GitHub + Bitbucket
Both real forges, one UX. (Not GitLab — built what was needed, no more.)
any model
Bring your own
Claude, Codex, Gemini, Cursor, opencode — reads context on stdin, prints comments on stdout.
gate
Severity-gated
Auto-approve only if nothing crosses your threshold. A human still sees the rest.
stats
Visibility
A dashboard shows what got reviewed, by whom, how much it helped.
08 · the bug that taught me about identity

A review for PR #2124 landed on PR #2125.

Two sessions on the same repo, two ports. One shared current-session pointer — rewritten on every browser status poll. The other PR's tab, polling in the background, flipped the pointer mid-review. The fix: stop the flip at its source, pin the reviewer's identity explicitly, force one chokepoint.

The wrong-PR race: a shared pointer, repointed by a background poll, misroutes the review
explicit identity beats ambient context — the reviewer checks its own pin before it ever reads the shared pointer.
09 · other bugs, other lessons

Each bug left a habit behind.

10 · where it landed

The v1 → v2 leap: twenty-one releases in four months.

Four months, twenty-one releases: one diff command to a self-hosted review bot
the leap — from reviewing one PR to reviewing a directory of repos unattended
11 · the itch, again — this time it's the AI

Same wall of hunks. Different reader this time.

Once auto was reviewing unattended, the AI was still cold-reading every diff — no orientation, just a wall of hunks. That is the exact problem that started this whole project, except now it was the AI stubbing its toe instead of me.

Before

Every session opens the same way: burn real effort and real tokens just figuring out which of forty changed files matter, and how they connect, before saying anything useful about the change itself.

After

A change map, computed locally and deterministically before any AI ever sees the diff — zero AI tokens spent finding out what could just be computed.

Appended automatically once a diff crosses 3 files / 80 changed lines — and pulled up on demand from a toolbar button, for a human, without starting a review at all.
12 · the change map

One diagram, per wired section, before a single line of review.

  • Which areas moved — and by how much.
  • Which areas are wired together by imports — labeled with the actual new symbols the diff introduces, not just an import count.
  • One coherent change, or several bundled into a PR — told apart automatically.
  • The AI's own review comment now opens with that same diagram before its line-by-line findings.
Journey epilogue: a human reading a cold diff with no orientation in v1.0 led branchdiff to fix that for humans, then an AI reading a cold diff with the same problem after auto shipped led to the change map's zero-token orientation, which raised the question of what it was actually costing, answered by stats tracking token and cost per tool and per repo
same shape, two readers: the human's wall of hunks, then the AI's
0
AI tokens spent
The map is pure computation, not a model call.
$ / pass
Cost, made visible
stats now tracks tokens and cost per pass, split by tool and by repo.
13 · the front doors

Then it moved to where you already were.

The last itch was not a feature — it was a doorway. Every workflow in this story starts with a command and a URL, and the people who stuck around were the ones already living in a terminal. So both groups got their own front door.

branchdiff view

A full-screen terminal picker — running sessions, a new comparison, Stats, History, Changelog, cron. Arrows or digits, no flags to remember. The terminal was already the front door; it just got a lobby.

The VS Code extension

The whole web UI in an editor tab. It adopts a running local server or starts one on its own; an activity-bar panel shows live counts. On Open VSX, or a .vsix from the releases page.

"Meet people where they are" turned out to be a loop, not a lesson — package managers, then platforms, then surfaces. Same server, same local data; which window you sit in stopped mattering.
14 · what building it taught me

The real payoff is the habits, not the features.

15 · recap

One stubborn idea, five months of Tuesdays.

branchdiff still does exactly what it did on day one — show you the real diff, not the phantom one. Everything else grew up around that single idea, each piece born from a friction I personally felt, over and over, until the friction was gone.

Deck Controls

SpaceNext slide
Previous slide
Home / EndFirst / last slide
19Jump to slide N
OOverview grid
GGo to slide
FToggle fullscreen
BBlackout screen
?Toggle this help
Click left / right halfPrev / next slide
Swipe (touch)Prev / next slide
Press ? or Esc to close

All slides — click to jump