Drop-in Compatibility as a Design Principle
Most of our portfolio follows a strict rule: keep the user's existing config files, the existing CLI flags, and the existing import statements working unchanged. This post explains why drop-in compatibility is the single highest-leverage design constraint we've found, walks through how rjest, rpytest, rninja, recurl, rewget, stout and the fast-* accelerators implement it, and is honest about the cases where we deliberately break the rule (agentvfs, ormai, mcp-pay).
Half the projects in the Neul Labs portfolio have the same shape: take a tool a lot of people already use, replace its implementation, and refuse to change anything a user can see. We call this drop-in compatibility, and it’s the single highest-leverage design constraint we’ve found.
This post explains the rule, walks through how a handful of projects implement it, and is honest about where we deliberately break it.
The rule
A project is a drop-in replacement if a user can get its benefits without changing:
- Their config files. Existing
jest.config.*, existingpyproject.toml, existingbuild.ninja, existing environment variables — all unchanged. - Their CLI surface. Same binary name on
$PATH(or a deliberate alias), same flags, same exit codes, same stdout/stderr semantics for scripts that grep them. - Their imports / API. If it’s a library, the public Python or TypeScript or Rust API stays identical at the call sites the user cares about.
- Their workflow. No new daemon they have to start by hand, no new credential they have to provision, no new dashboard they have to log into to make basic things work.
Anything that violates one of these four becomes a project-level objection in our review. We allow exceptions, but we name them out loud (more on that at the end).
Why we bias this hard toward compatibility
There are easier ways to ship a “better X.” You can fork the API and call it Y. You can introduce new config sections. You can require a daemon and a service account. Plenty of well-loved tools do exactly that.
We don’t, for three reasons.
First, activation energy is the real benchmark. Every project README we ship has a Quick Start section, and most of them are some variant of install, run, done. From rpytest’s README:
# Install and run - that's it
pip install rpytest
rpytest
That is the entire interaction. No config flag for “compatibility mode,” no opt-in for the daemon, no migration guide. A developer can find out whether rpytest is useful to them in approximately the same time it takes to run pytest once. The speed number on the rpytest README — 1.9× wall-clock on a 480-test suite, 5.8× less CLI memory — only matters if they ever get that far. Compatibility is what gets them there.
Second, the marginal user is more conservative than the marginal blog post. Hacker News rewards novelty. Production engineering rewards reversibility. The user who might try our tool already has a CI pipeline that grep’s the test output. They have a coworker who set up the build config and left. They have a wget invocation that has been working for four years and they would prefer not to think about it. The thing we can offer them is “this is the same as what you have, but faster” — and they can roll it back tomorrow by uninstalling.
Third, compatibility forces honest engineering. If we are allowed to change the API, we will paper over hard edge cases by inventing new options. If we are not, we have to handle them the way the upstream tool does. That makes the surface harder to build, but the resulting tool is more truthful about what it does.
How a few projects implement it
rjest: read the user’s Jest config, don’t replace it
rjest is a Rust daemon that caches SWC transforms and pre-warms Node workers across test invocations. The temptation, when writing a Jest replacement, is to invent your own config format that’s “cleaner.” The rjest README is explicit about not doing that:
It reads your existing
jest.config.*files with zero config changes and supports the same CLI flags you already use:--watch,--coverage,--runInBand,--testNamePattern,--json,--machine.
The README claims warm runs in roughly 14ms — ~100× faster than standard Jest. We will let your CI suite be the real benchmark. What matters from a design standpoint is the second half: the user does not learn a new config language, and the same --testNamePattern flag they were typing yesterday still works today.
There are multiple install paths — npm install -D rjest-install, brew install, cargo install, pip install rjest-install — because Jest users come from multiple ecosystems and we want each to be able to install from a place they already trust.
rpytest: same pytest, persistent daemon
rpytest makes the same trade. The README’s wording is the cleanest one-liner of the rule in the whole portfolio:
Run your pytest suite faster. Change nothing.
The speed comes from keeping Python warm between runs in a persistent daemon, so you stop paying interpreter startup cost on every invocation. The user is not aware the daemon is there — it starts on first use and stays warm. Their pytest fixtures, plugins, conftest.py and CLI flags are all unchanged.
The wall-clock improvement in the README is 1.9× on a 480-test suite, and the memory improvement is 5.8×. That’s a modest speedup by accelerator standards, and the README’s honesty about that is part of the point: drop-in compatibility means we cannot juice numbers by changing the workload. The number is what the user actually gets.
rninja: binary-compatible Ninja
rninja is a Rust-powered drop-in for the Ninja build system. The README headline:
Build faster. Cache smarter. Drop-in ready. … Cut your build times without changing your build files.
Ninja is the easiest case in some ways — the file format is simple and well-documented — but it’s still surprising how many “alternatives” pick this moment to introduce a new manifest schema. We didn’t. Your build.ninja keeps working. The caching and modern scheduling are additive.
recurl and rewget: the curl/wget surface, with anti-bot escalation
recurl and rewget are the most fun version of this principle. The READMEs are unambiguous:
curl that just works. Drop-in replacement with automatic anti-bot bypass.
recurl runs real curl, detects when you’re blocked, and automatically escalates through impersonation and headless browser rendering. Same curl syntax you know. No code changes.
# This just works, even on Cloudflare-protected sites
recurl https://protected-site.com/api/data
The trick is that only the failure path is different. If your request would have worked under plain curl, it goes through plain curl. If it’s blocked, recurl escalates through TLS impersonation and a headless browser. From the script’s perspective the binary just has a higher success rate — same flags, same exit codes, same body in stdout.
stout: the Homebrew-compatible package manager that drops Ruby
stout is a fast, Rust-based, Homebrew-compatible package manager. Compatibility here means accepting Homebrew formulae and the user’s mental model of brew install <thing>. The implementation is wholly different — the README explains:
The secret? stout eliminates Ruby entirely. It uses a pre-computed SQLite index with FTS5 full-text search, fetches only what it needs, and downloads bottles in parallel.
A user who switches from Homebrew to stout doesn’t learn a new tap concept, a new formula format, or a new install command. The implementation underneath is unrecognisable.
The fast-* accelerators: import-line compatibility
The Python AI accelerators (fast-litellm, fast-langgraph, fast-crewai, fast-axolotl) translate the drop-in idea to library APIs. The pattern is the same across all four — import fast_<lib> before import <lib>, and the upstream library’s public surface is unchanged:
import fast_axolotl # auto-installs the acceleration shim
import axolotl # now accelerated
You can read the fast-langgraph benchmark write-up and why we chose Rust for the deeper picture.
What it costs us
Drop-in compatibility is not free. The costs:
- You inherit the upstream’s mistakes. If Jest has a weird
--testNamePatternregex semantics, rjest matches it. If pytest’s collection algorithm has a particular ordering quirk, rpytest preserves it. If LiteLLM’s connection-pool behavior is undocumented for some provider, fast-litellm replicates it. The upstream’s bug surface is now your bug surface. - You can’t fix bad defaults. When the upstream tool’s default is genuinely wrong, the most you can do is add a new flag with a different default. You cannot quietly flip the default for “your” users.
- You take on a maintenance commitment to a project you don’t control. When upstream ships a new minor version, you have a deadline for compatibility you didn’t choose.
We accept these in exchange for adoption velocity. The empirical pattern is clear enough that we now treat compatibility as the default and any deviation as the exception.
Where we deliberately break the rule
A handful of projects intentionally do not pursue drop-in compatibility, and we name them so the rule stays sharp:
- agentvfs invents a new surface — there is no widely-used “workspace runtime for AI agents” to be compatible with. The README describes it as “a proxy boundary [that] mediates between your agent and the workspace … governs what commands run, creates checkpoints, and reports filesystem deltas.” We are building a primitive that didn’t exist.
- ormai sits on top of existing ORMs but introduces a new policy surface. The whole point of the project is to restrict what the underlying ORM exposes, which is an explicit deviation from the drop-in idea. The README is candid: “OrmAI solves these [scary scenarios] at the ORM layer — not the prompt layer.”
- mcp-pay introduces a new manifest (
mcp-pay.json) and a new HTTP 402 protocol flow. There is no existing “payment-aware MCP” to be compatible with — it’s the standard we’re trying to seed. - brat introduces a new orchestrator concept (the “Mayor” plus crash-safe state via the Grite append-only event log). It coordinates existing agents (Claude Code, Aider, OpenCode) without trying to mimic any one of them.
In each case the project is creating a primitive, not replacing an existing implementation. That’s the right time to deviate.
The default
When in doubt, we keep the user’s config, their CLI, and their imports. When that’s not possible — because the primitive doesn’t exist yet — we admit it explicitly and move on. The portfolio reflects the rule strongly enough that you can tell which projects are which from the first paragraph of each README. That, as much as anything else, is the design system at Neul Labs.
If you build developer tools, try the constraint on your next release. Forbid yourself from changing the config schema, the CLI surface, or the public imports for a quarter. The work gets harder. The adoption curve gets straighter.