A commit gate for TypeScript projects.
It owns the invocation of ESLint, react-doctor, fallow and Stryker: which tool runs, in what order, scoped to what, with what ceiling on resources. The verdict is their exit code, passed through untouched.
$ dharness sync dharness 1.7.5 · sync · ~/projects/your-app js project repository root package manager yarn test runner vitest owned files .dharness/ ■ 11 steps · 5 applied · 3 delegated · 3 satisfied · 0 failed 6.76s ── Applied (5) ── ✓ 1/11 install what this project is missing 6.73s │ ➤ YN0013: │ 33 packages were added to the project. │ installed [email protected] ~ package.json ~ yarn.lock ✓ 2/11 write the files dharness owns 0.03s + .dharness/lefthook.yml + .dharness/fallow.jsonc + .dharness/eslint.config.mjs + .dharness/rules.json ✓ 3/11 point .fallowrc.json at the file dharness owns 0.00s ✓ 6/11 point eslint.config.js at the file dharness owns 0.00s ✓ 8/11 give the agent fallow’s own tools 0.00s ── Left to you (3) ── ! 9/11 wire the gate into git nothing answers: there is no lefthook config, no .husky/ and no lefthook binary. Choosing a hook manager is a decision this project has not made, and not a default dharness gets to pick. … steps 10, 11 and the satisfied three elided … ✓ 5 applied · 3 delegated · 3 satisfied · 0 failed exit 0 next wire the gate into gitA failure stops the run and skips every stage behind it, and on a gate that runs at every commit that cut is most of the saving. Each stage below carries the median it was measured at and what puts it there.
--no-warn-ignored <staged files>Lint over the exact staged list, resolved locally
It runs from the version your project installed, over the exact staged list, using the config dharness owns with the preset your framework recommends already in it.
Runs as soon as your project installs it
--staged --no-dead-code --no-score --no-supply-chain -yReact semantics, over the staged change only
Bounded by what is actually being committed, and it is the only stage here for which that is true. Its analysis stays on your machine.
audit --changed-since HEAD --diff-stdinThe repository graph, judged on what the change introduces
The staged diff is handed to it on stdin, so the graph is judged on the change you are about to commit. It exits 1 on a fail verdict and gates on new findings alone.
Runs as soon as the repository has a commit to compare against
dupesThe duplication ceiling, enforced over the whole repository
The ceiling lives in the config dharness owns, where your project can read it and override it.
Runs as soon as the repository has a commit to compare against
Medians of three runs each, one reference project, the same explicit staged file list for every stage. 12 August 2026.
First failureexit 1fallow auditWhat does this change introduce?
exit 0fallow dupesHow much duplication does the repository carry?
exit 1The exit code is the answer, propagated untouched. A failing stage names every stage it skipped, so all four are always accounted for.
$ dharness checkno staged source files, nothing to check $ echo $?0The whole surface fits on this screen.
$ dharness syncSet the project up, and keep it that way
WritesEvery run derives its whole plan from the repository as it is right now, so running it again after months reports the drift.
--format <human|json> (default human)eslint --print-configOne path per source extension, up to eight of them. A green sync is one whose config has already been loaded and read back.
| What the run does | What a failure leaves |
|---|---|
| Packages installed, files written into .dharness/ | Exactly what this run added, removed |
| The manifest and the lockfile edited | Both restored byte for byte |
$ dharness checkThe commit gate itself
Writes nothingIt stops at the first failure, names every stage it skipped, and hands you that tool’s own help in the form your project uses. It also names which kind of failure it got: an ESLint exit 2 is a configuration that never loaded, so no staged file was linted and the answer is in the config.
$ dharness mutate <path...>Find out whether these files’ tests would notice the code breaking
Writes nothingA path can name lines instead of the whole file, as `src/thing.ts:12-40`, and the verdict then covers exactly those lines. It runs the Stryker your project installed, the only one that can resolve your own TypeScript.
--dry-run--concurrency <n> (default 2)Every tool sizes its parallelism against the whole machine, and more workers measured slower on a small scope.
--upgrade--freshResults accumulate in `.git/dharness/`, so Stryker’s table can cover more files than you asked about. This measures only the paths you named.
$ dharness versionPrint version
Writes nothingReleases are cut by release-please from the conventional commits on main, so the number this prints names a tagged release with a changelog entry behind it.
Four tools that already exist, each owning one diagnostic of its own.
Does this file follow the rules this stack recommends?
| Framework | Preset |
|---|---|
| Next.js | eslint-config-next/core-web-vitals |
| Next.js, once a tsconfig.json exists | eslint-config-next/typescript |
| Expo | eslint-config-expo/flat.js |
The version comes from the framework that ships the preset. Your `eslint.config.js` gains one reference line, inside a marked region rewritten on every sync, pointing at `.dharness/eslint.config.mjs` or `.cjs`, whichever matches your project.
Visit the toolWhat does this repository’s graph look like?
| Import | In its boundaries |
|---|---|
| A type-only import | Crosses the zone |
| An import of an external package | Not counted as a crossing |
It prints its resolved entry points and warns when a zone matches nothing.
Visit the toolIs this React code semantically sound?
It reaches the project twice: the CLI in the gate, and `eslint-plugin-react-doctor` in the config dharness owns, so the same analysis answers while you type and again at the commit. Its rule severity accepts only `error`, `warn` or `off`, and that single constraint produced the companion package below.
Visit the toolWould the tests notice the code breaking?
The verdict comes from the report it writes, so the number that decides a run is one you can read.
Visit the tooldharness-eslint-pluginSize, documentation and folder shape need a number or a policy to check against, and this package is where a project states one.
dharness/max-file-linesA file past the ceiling this project set.dharness/require-jsdocA declaration at the top of a file with nothing saying what it is for.dharness/require-variable-jsdocA top-level variable with no JSDoc immediately above it.dharness/pure-index-barrelA barrel that does anything other than re-export.dharness/role-file-shapeA declaration a role file’s own name did not promise.dharness/folder-ownershipA folder that splits a module into role files and publishes no index.dharness asks `git ls-files -- */index.ts */index.tsx` and writes this rule as `error` where the tree already publishes a barrel, `off` where it does not.{
"schema": "dharness.rules/v1",
"maxFileLines": 500,
"roleSuffixes": [".types.ts", ".constants.ts", ".helpers.ts", ".schema.ts"]
}The numbers live in `.dharness/rules.json`, so one project can differ from another without publishing a new version. A missing or unreadable file falls back to the defaults, and every other rule keeps running.
dharness/max-file-linesmaxFileLinesdharness/folder-ownershiproleSuffixesOne `plugins: { dharness: plugin }` registration in the config dharness owns is the whole wiring, and it puts the six rules wherever ESLint already runs in that project. The package ships a CommonJS entry and an ESM one, so it loads in whichever module system a project is written in.
One static binary for Linux, macOS and Windows, on amd64 and arm64.
$ go install github.com/Disble/dharness/cmd/dharness@latestThe CLI itself: its source, its tagged releases and the dated learning log.
The six rules ESLint loads from the owned config, shipped with a CommonJS entry and an ESM one.
The Go mutation engine this repository runs its own staged changes through.