- Node.js (v22+ recommended)
- npm
- Git
# Install dependencies
npm install
# Start all watchers (run this in the diffity repo)
npm run devThis automatically creates the diffity-dev binary, builds skills, adds .bin to your PATH (in ~/.zshrc or ~/.bashrc), and starts five concurrent processes:
| Process | What it does |
|---|---|
| parser | tsc --watch on @diffity/parser |
| git | tsc --watch on @diffity/git |
| cli | tsc --watch on the CLI package |
| ui | vite build --watch on @diffity/ui |
| skills | Rebuilds Claude skills on change |
If this is your first time, source your shell profile to pick up the PATH change:
source ~/.zshrc # or ~/.bashrcThen, in any git repository:
diffity-devThis opens a diff viewer for that repo's working tree changes.
The UI uses vite build --watch instead of vite dev. This is intentional — vite dev serves files from memory and never writes to disk, but the CLI server serves static files from packages/cli/dist/ui/. Using vite build --watch rebuilds the output on every change so the CLI can serve it. Refresh the browser to see changes.
tsc --watch recompiles TypeScript to dist/ on save. The diffity-dev binary uses node --watch-path=packages/cli/dist which auto-restarts the Node process when any file in dist/ changes. The port is persisted across restarts — the server retries the same port if it's briefly held by the old process.
diffity-dev is a shell script (not a symlink) created by scripts/link-dev.ts. It runs:
node --watch-path=<dist-dir> <cli-entry> --no-open "$@"- Shell script, not symlink — a symlink to
dist/index.jswould load the CLI once and never pick up server changes. The shell script wraps it withnode --watch-pathso it restarts on recompilation. --watch-path— restarts the process whentsc --watchwrites new files todist/.--no-open— prevents opening a new browser tab on every restart. Open the URL manually on first run.
diffity/
├── packages/
│ ├── api/ # Wire types shared by server and UI
│ ├── cli/ # CLI server and entry point
│ │ ├── src/
│ │ └── dist/
│ │ ├── index.js # CLI binary
│ │ └── ui/ # Built UI (served as static files)
│ ├── git/ # Git operations (execSync wrappers)
│ ├── github/ # GitHub PR integration (gh wrappers)
│ ├── parser/ # Diff parsing library
│ └── ui/ # React frontend (Vite + Tailwind)
├── scripts/
│ ├── dev.ts # Starts all watchers concurrently
│ ├── link-dev.ts # Creates the diffity-dev shell script
│ ├── build.ts # Production build (all packages in order)
│ └── build-skills.ts
└── .bin/
└── diffity-dev # Generated shell script for development
diffity (cli) ───► @diffity/api, @diffity/git, @diffity/github, @diffity/parser
@diffity/ui ─────► @diffity/api, @diffity/parser
@diffity/github ─► @diffity/api, @diffity/parser
@diffity/api ────► @diffity/parser (types only)
@diffity/git ────► nothing
The UI builds into packages/cli/dist/ui/ so the CLI can serve it as static files. In production, everything ships as a single diffity npm package.
# Full production build (all packages in dependency order)
npm run build
# Build a single package
npm run build -w @diffity/parser
npm run build -w @diffity/git
npm run build -w @diffity/ui
npm run build -w @naturalcycles/diffity# Run all tests
npm run test
# Run tests for a specific package
npm run test -w @diffity/parser
npm run test -w @diffity/ui
# Watch mode
npm run test:watch -w @diffity/parser
npm run test:watch -w @diffity/uiscripts/inbox-compare.ts re-prepares a pull request the inbox has already reviewed, with a
different model or effort, and puts the two sets of findings side by side. It is how a change to
agent.model is decided: by what the candidate finds, not by what it costs.
npm run build # the script runs the built CLI, so build first
npx tsx scripts/inbox-compare.ts NaturalCycles/NCBackend3#14550 --model opus
npx tsx scripts/inbox-compare.ts NaturalCycles/NCBackend3#14550 --effort medium --out /tmp/14550.mdThe baseline is the newest bundle for that pull request under ~/.diffity/inbox/bundles
(--bundles-dir to look elsewhere, --head <sha> to pick an older one). The candidate gets a
scratch worktree and its own diffity data directory under a fresh temp directory (--scratch to
name it), so nothing is written into ~/.diffity and a running diffity inbox is undisturbed. The
worktree is removed afterwards unless --keep is passed, which leaves it in place so the
candidate's own review can be opened in the browser. One invocation is one agent run, and it takes
as long as a real preparation — up to half an hour.
The candidate is pinned to the baseline's head, so a pull request that has moved on since — or has
merged — can still be compared. An earlier head usually comes along with the pull request's own
ref; when it does not, it is fetched by sha, which the forge serves for any commit reachable from a
ref it advertises. A head that was force-pushed away is gone for good, and the run is refused with
the baseline's head <sha> is no longer reachable from origin.
Reading the table: each severity row is baseline count | reproduced, new. Reproduced means a
candidate finding landed on the same file with an overlapping line range — a one-line finding
counts as its line give or take two. New counts candidate findings no baseline finding covers;
some are real, some are noise, which is what the finding list underneath is for. The cost / time
row is the candidate's own run: two models are not comparable on the baseline's, which predates
the run log. Exit code 0 is a completed comparison, 2 a candidate that skipped or failed, 1 a
usage error. --json prints the same numbers as one object.
The baseline bundles were prepared by the old pipeline, which loaded the reviewer's own settings, skills and MCP servers and let the agent run the repository's toolchain. A difference between the columns is therefore prompt and model, not model alone.
diffity-dev # Working tree changes
diffity-dev HEAD~1 # Last commit vs working tree
diffity-dev HEAD~3 # Last 3 commits vs working tree
diffity-dev main..feature # Compare branches
diffity-dev --port 3000 # Custom portIf diffity-dev fails with EADDRINUSE, a previous process is still running:
# Find and kill the process
lsof -i :5391
kill <PID>The server retries the same port up to 30 times (15 seconds) on startup to handle the brief overlap during --watch restarts. But if a completely separate process holds the port, you need to kill it manually.
- Make sure
npm run devis running in the diffity repo - Check that the relevant watcher (ui/cli/parser/git) isn't showing errors
- Refresh the browser — there's no HMR in this setup
- For server changes, wait for the
--watchrestart (you'll see the diffity banner re-print in the terminal wherediffity-devis running)