Verified AI migrations for dependency upgrades.
Emend finds which of your lines a version bump actually breaks, migrates them with an agent in a throwaway worktree, then proves the result compiles and your tests still pass — or refuses to call it fixed.

That "+ 1930" line is the product. zod 4 ships 1,934 breaking changes. Four of them touch this repository. Everything downstream operates on those four — and on the line below the summary, which says how much of the repository went unexamined, because a summary that cannot be wrong about its own coverage is the only kind worth reading.
Try it in 30 seconds
No API key. No config. scan is fully deterministic and never calls a model.
npx emend-cli@latest demo ./emend-demo
npx emend-cli@latest scan ./emend-demo --only zod
That scaffolds a repo with genuine dependency drift and shows you exactly the output above. Requires Node 22.6+.
Then point it at something real:
npx emend-cli@latest scan . --only zod,axios
Why this isn't another AI migration tool
Most tools in this category end at "the model edited your files." Emend treats that as the start of the problem, because a model that edits confidently and wrongly is worse than one that does nothing.
Every migration runs in a throwaway git worktree:
- baseline — typecheck + tests run before any edit
- apply the edits, bump the dependency
- post — typecheck + tests run again, and the two are compared
Without step 1, a repository that was already failing has its pre-existing failures
blamed on the migration. Emend distinguishes verified / regression /
pre-existing-failure / typecheck-only / unverified, and never reports the last
three as success. Nothing touches your working tree; the worktree is thrown away
unless it verifies.
A second pass — a read-only behaviour review — then answers what a green build cannot: whether the change still means the same thing. Its own edits are re-verified, and reverted wholesale if they don't hold.
Honesty rules, enforced in code
Not conventions. These are properties the test suite holds to:
- A package with no type declarations is
unanalyzable, never "clean". - A truncated surface walk suppresses removal reporting — absence past a cutoff is not deletion.
- Verification that didn't run is
unverified, never "passing". - No test script means
typecheck-only, never "tests pass". - Breaking changes detected but not located in your code are counted and reported, not silently dropped — static analysis cannot see
client[name](). - Skipped packages are counted separately from analyzed ones. Skipped ≠ clean.
- A run that cannot reach a model repairs nothing and says so. Could not fix is not the same as nothing to fix.
How it works
package.json + node_modules npm registry
│ │
▼ ▼
installed version ────────────► .d.ts of BOTH versions
│
▼
public API surface diff
(removed / signature / deprecated)
│
TypeScript program over your repo │
(imports + type-checker resolution) │
│ │
└──���───────────► INTERSECTION ◄──────┘
│
▼
FINDINGS (symbol, file:line, severity)
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
deterministic plan harness (opencode+MCP) dashboard / PR
└──────────► isolated git worktree ◄──────┘
│
baseline → apply → verify → compare
│
▼
behaviour review (read-only model)
│
▼
verified · regression · unverified · unreviewed
Why .d.ts diffing. Other approaches read version numbers (no idea what changed),
OpenAPI specs (most SDK surface isn't in them), or changelogs (prose, and frequently
silent about breaks). Type declarations are machine-readable, versioned, exhaustive, and
already published by nearly every TypeScript SDK — no provider cooperation required.
Why the intersection matters. A diff alone is noise. Crossing it with real call sites turns "1,934 breaking changes" into a four-item work order with file and line numbers.
The deterministic core — detection, localisation, rename-class migrations, verification — runs with no model involved. The model runs only where the answer is a judgement. Full design in docs/architecture.md.
What it checks
| Tier | What it reads | Command |
|---|---|---|
| Typed packages | Published .d.ts of both versions, crossed with your call sites |
emend scan <repo> |
| HTTP contracts | Each vendor's own published OpenAPI description, vs. your outbound calls | emend scan <repo> --contracts |
| Version pins | Lockfile vs. the copies in Dockerfiles, CI matrices, .nvmrc |
emend pins <repo> |
| Vulnerabilities | The installed tree against OSV — no key, no rate limit | emend scan <repo> --vulns |
Calls to APIs you don't have a package for
Half the contracts a service depends on aren't packages at all:
const res = await fetch(`https://api.vendor.com/v1/audiences/${id}/contacts`);
No .d.ts, no lockfile entry, no type — a URL is a string that compiles whatever it
says. --contracts resolves each vendor's own OpenAPI description and reports calls
reaching a route the description no longer contains, guarded by provenance (is this
really the vendor speaking?), currency (a first-party spec last touched in 2020
asserts nothing), and coverage (one host often serves several APIs).
A finding here is a lead to verify, not a proof — providers do serve endpoints they never wrote down, which is why it says not described by rather than removed. See docs/limitations.md.
Versions your repo writes down twice
A version lives in the lockfile, where the package manager keeps it honest. The same
version copied into a Docker tag, an .nvmrc, or a CI matrix is a copy, and nothing
keeps a copy honest.
$ emend pins ./my-service
drift node → 22 (the declared engines.node)
→ Dockerfile:1 node:18-alpine
→ .github/workflows/ci.yml:6 node-version: '22'
VERIFIED 2 edit(s) applied of 1 repairable conflict(s)
Three files declaring three versions with nothing to arbitrate are reported as disagreeing and not repaired — picking a winner would be guessing, and that decision is yours.
Commands
| Command | What it does |
|---|---|
emend demo [dir] |
Scaffold a demo repo with real drift |
emend scan <repo> |
Find API changes that intersect your code |
emend fix <repo> |
Plan, apply, and verify migrations |
emend pr <repo> --finding <id> |
Render the pull request (dry run by default) |
emend pins <repo> |
Repair drifted version pins — no model involved |
emend eval |
Measure the agent against a corpus |
emend serve |
Local dashboard, or a hosted monitor with GitHub App credentials |
emend models |
List models your LLM provider serves |
emend store list |
What is stored locally |
emend mcp |
Serve Emend's tools over MCP, so a coding agent can drive it |
Useful flags: --only pkg,pkg, --all, --json, --no-dev, --contracts,
--features, --freshness, --vulns, --lint (scan); --finding <id>, --no-agent,
--no-review, --untrusted, --keep (fix). emend --help has the full list.
emend mcp exposes seven tools — scan, plan_remediation, fix_vulnerability,
fix_package, verify, advisory_status, impact — and every one returns what was
measured, never a judgement. An agent claiming a vulnerability is fixed has to call
advisory_status and read the lockfile's answer.
Repairing, not just scanning
scan needs nothing. Repairing additionally needs the
opencode binary on your PATH and a key for a model it can
reach. Emend's reviews and PR summaries work with any OpenAI-compatible endpoint:
export EMEND_LLM_PROVIDER=nebius # or fireworks, together, groq,
# deepinfra, openrouter, ollama, vllm
export NEBIUS_API_KEY=...
emend models # see what your provider serves today
export EMEND_LLM_MODEL=<id from above>
emend fix ./my-service
Or point it anywhere directly — a local Ollama works:
export EMEND_LLM_BASE_URL=http://localhost:11434/v1
export EMEND_LLM_MODEL=qwen3-coder:30b
The recommended defaults are open-weight. This follows the literature rather than the intuition: Byam (arXiv 2505.07522) found end-to-end LLM migration fully repaired only 27% of builds, improving markedly when given API diffs, failing lines and compiler feedback — all of which the harness is given. BigBag (arXiv 2606.24446) drives its agent through a harness for 78.6%.
Prompt changes are cheap to make and hard to judge, so there's a scored corpus: docs/evaluating-the-agent.md.
Can I use this at work?
Yes — running Emend on your own code triggers no obligations at all.
Emend is AGPL-3.0. The clause people are usually worried about is §13, Remote Network Interaction, and it is narrower than its reputation:
| What you're doing | What you owe |
|---|---|
Running emend scan / fix on your own repositories |
Nothing. |
| Running it in your own CI, on private code | Nothing. |
| Modifying it for internal use | Nothing, as long as it stays internal. |
| Running a modified Emend as a service for other people | Offer those users your modified source. |
| Embedding it in a product you ship without AGPL terms | Get a commercial licence. |
AGPL is a copyleft on distribution and network service, not on the code it reads. Emend
analysing your repository no more licences your repository than tsc does.
A separate commercial licence is available for embedding Emend without AGPL obligations — reach out via issues. That option only exists because contributions are collected under a CLA; see CONTRIBUTING.md for why it's collected before the first merge rather than after.
Running it as a service
emend serve is a local dashboard, and becomes a hosted monitor when GitHub App
credentials are present:
export EMEND_GITHUB_APP_ID=...
export EMEND_GITHUB_PRIVATE_KEY="$(cat emend.private-key.pem)"
export EMEND_GITHUB_WEBHOOK_SECRET=...
emend serve --port 8080 --host 0.0.0.0 # POST /webhook is now live
Installing it on a repository queues a scan; pushes to the default branch queue another.
Each scan reconstructs node_modules from the lockfile, finds the drift, migrates what
it can, and opens a draft pull request per package.
Why it can run untrusted repositories in-process: analysis never executes anything
from the repository or its dependency tree. Dependencies are symlinked from a tarball
cache rather than installed, the bump passes --ignore-scripts, and the repository's
test script is never invoked. The cost is that hosted verification is typecheck-only —
Emend says so on the pull request rather than implying more, and reads the real verdict
back from the check_suite webhook when your CI runs the tests.
Step-by-step: docs/github-app-setup.md · docs/deployment.md
Status
MVP / proof of concept. TypeScript + npm + GitHub only. Scanning, migration and pull requests are exercised against a real private repository; the GitHub App token exchange is the one link only a registered App can validate.
Emend is deliberately explicit about where it can be wrong or silent — docs/limitations.md documents each case, measured rather than hypothesised. If you only read one other page, read that one.
Issues and PRs welcome — CONTRIBUTING.md has the ground rules
(npm run typecheck and npm test must be green; prompt changes need an emend eval
table before and after).
Docs
| Getting started | Configure a model, scan a real repository, read the output |
| Architecture | The three tiers of evidence, and where the model deliberately does not participate |
| Known limitations | Every case where Emend can be wrong or silent, measured |
| Evaluating the agent | The scored corpus, and why Pass and Clean are different columns |
| GitHub App setup | Registering the App, step by step |
| Deployment | Sizing, hosting, systemd/TLS |
Build from source:
git clone https://github.com/diyanbogdanov/emend.git && cd emend && npm install
npm run typecheck && npm test
A checkout runs its sources directly via native type stripping — no build step needed.
Comments