# Changelog
Source: https://docs.refactron.dev/changelog
Release history for Refactron, newest first.
Refactron follows [Semantic Versioning](https://semver.org/). The full,
unabridged log lives in [`CHANGELOG.md`](https://github.com/Refactron-ai/refactron/blob/main/CHANGELOG.md).
**Two fixes to what the verified test suite can reach.**
Neither is exploitable without already being able to hand Refactron a diff or a
test command, which is the normal mode of use. That is the point: the CI gate
this tool is built for verifies untrusted pull requests, so "the attacker
controls the input" is the design assumption.
**The verified suite no longer inherits your credentials.** Refactron runs your
repository's own test suite, and the diff under verification defines that
suite. It was handed the full parent environment. Reproduced: a test read
`REFACTRON_TOKEN`, `GITHUB_TOKEN`, `NPM_TOKEN` and `AWS_SECRET_ACCESS_KEY` in
plaintext. This matters most in the deployment Refactron is built for, a CI
gate holding the credentials of the repository being protected.
Credentials are now removed from the environment of every spawn that executes
your suite — the common names, plus anything ending in `_TOKEN`, `_SECRET`,
`_API_KEY`, `_PASSWORD` or `_CREDENTIALS`. `PATH`, `HOME`, `VIRTUAL_ENV` and
the rest of your toolchain are untouched.
This is redaction, not a sandbox. Running `verify-diff` still runs your tests,
exactly as running them yourself does. `SECURITY.md` now says so plainly.
**A diff can no longer name a file outside the repository.** The path came from
the diff's own `+++` header and was read with no containment check, so a diff
naming `../../../.ssh/id_rsa` caused Refactron to open it. The shadow tree
blocked the resulting write, but the read had already happened, and whether the
patch applied was an oracle for the file's contents. Containment now runs at
intake, before the first read, and resolves symlinks — a link planted inside
the repository defeated a purely lexical check.
Both fixes shipped with a bypass of themselves, found in review of the release
and fixed in it. The coverage probe still ran unredacted, and it executes a
`coverage.py` planted at your repository root. Recording that because the
pattern is the lesson: a redaction covering two of three spawns is not a
redaction.
`SECURITY.md` was also rewritten. The previous version described the
refactoring product removed in 0.4.0, and it now documents what actually ships,
including a section on what Refactron explicitly does **not** defend.
**Security: Refactron could write to your working tree. Update immediately.**
The shadow tree was populated with **hardlinks**, so every file your diff did
not change shared an inode with your real file. The tests gate runs the suite
*as the diff defines it* — and a diff may edit `conftest.py`, a fixture, or any
test file — so any in-place write from that suite went straight into your
repository. The verdict said `SAFE` while it happened.
**No attacker required.** Any suite with a snapshot updater, a golden-file
regenerator, or a test that writes a fixture could silently modify the
repository being verified.
Affected: every published version, `0.1.0-beta.2` through `0.4.1`, on npm and
PyPI. Reachable from the CLI and from the MCP `verify_change` tool, which
applies no authentication. It contradicts the guarantee the README, the docs and
`SECURITY.md` all state — "your working tree is never touched" — which was false
for the entire life of the product.
Fixed by copying instead of hardlinking, with copy-on-write where the
filesystem supports it. Two related fixes ship with it: shadow-tree containment
could be escaped by a repository symlink pointing outside itself, and a rejected
change left a full copy of your source in the temp directory. Both now have
tests; neither did before, which is how this survived four minor releases.
**Also fixed: four more false `SAFE` verdicts** in the 0.4.1 narrowing check,
found by an adversarial review of that release. The serious one is that the
command scanner stopped at the first flag it did not recognise and discarded
any filter after it — so `pytest -q --durations-min=0.5 tests/test_a.py`, using
a stock pytest flag, returned `SAFE`. Also `unittest discover -s tests/unit`,
`pytest --cov --collect-only`, and a bare `runtests.py` being granted the
classifier's strongest claim about a file it never opened.
The docs no longer state the narrowing check as an absolute: it is a strong
check on the runners and flags Refactron knows, not a guarantee. Run the bare
command if you need certainty.
**Five false `SAFE` verdicts fixed, and `SAFE` now means something narrower.**
The report's shape did not change, so the version number alone will not tell
you that — read this entry.
Each of these was reproduced before being fixed. In every case the suite
**passed** and coverage **was measured**, and the verdict was still wrong:
| Command | Was | Now |
| -------------------------------------- | ------------------------------------- | ---------- |
| `pytest -q tests/test_scale.py` | `SAFE` on a change that breaks a test | `UNPROVEN` |
| `pytest -q --collect-only` | `SAFE` while running **zero** tests | `UNPROVEN` |
| `PYTEST_ADDOPTS="-k x" pytest -q` | `SAFE` while running one test | `UNPROVEN` |
| `python3 -m unittest tests.test_scale` | `SAFE` on a change the suite catches | `UNPROVEN` |
| 40 statements changed, 1 executed | `SAFE`, "the changed code is covered" | `UNPROVEN` |
Two rules changed to close them. A test command that names a **subset** of
your suite now caps the verdict at `UNPROVEN`, and `SAFE` requires **every
changed statement a test could reach** to have run, rather than one per file.
Statements coverage.py excludes (`# pragma: no cover`, `if TYPE_CHECKING:`)
are subtracted rather than held against you.
Every verdict that moves, moves from `SAFE` toward `UNPROVEN`. Nothing that was
`UNSAFE` or `UNPROVEN` can become `SAFE`. Exit codes are unchanged, so no CI
pipeline breaks — pipelines that read the verdict string will see more
`UNPROVEN`.
**If you acted on a `SAFE` from 0.4.0 or earlier** for a change verified with a
narrowed test command, or one whose `coverage.changedStatements` showed
`covered < total`, that verdict claimed more than it proved. Re-verify.
Two new report fields: `testScope` (was the command `full`, `narrowed` or
`unknown`) and `engineVersion` (which rules produced this verdict —
`reportVersion` only tells you which shape you hold). `unittest` joins
`pytest`, `vitest` and `jest` as a recognised runner. Also clears two high
security advisories, lockfile-only.
**Breaking.** Refactron is now only a verification layer. `analyze`, `run`,
`document`, `rollback`, `preflight`, `init`, the interactive TUI and the 20 AST
transforms have been removed from this package, along with blast-radius scoring
and the tier taxonomy. They were the demo of the verification engine, not the
product.
If you use any of them, pin `refactron@0.3.1`. The code is archived with its
full history and is not currently published under any name.
`verify-diff` and the MCP `verify_change` tool are unchanged. Bare `refactron`
now prints help and exits 2 instead of opening the TUI, `refactron login` is a
real command for the first time, and `import { verifyDiff } from 'refactron'`
resolves for the first time. Full detail in
[`CHANGELOG.md`](https://github.com/Refactron-ai/refactron/blob/main/CHANGELOG.md).
Two false `SAFE` verdicts, found and fixed. Both had the same shape: coverage
measured a different program than the tests gate ran, then reported the
changed lines as covered. A false `SAFE` is the one defect this product
cannot have, so upgrade rather than pin: `npm install -g refactron@0.3.1`.
A `testCmd` carrying a leading `PYTHONPATH=` no longer silently disables
coverage measurement, and a console entry point is now resolved the way the
shell resolves it, or declined. A `testCmd` naming an entry point that cannot
be resolved reports `UNPROVEN` rather than `SAFE`, which is a verdict change
in the safe direction. Full detail in
[`CHANGELOG.md`](https://github.com/Refactron-ai/refactron/blob/main/CHANGELOG.md).
The verification layer ships. Verify any diff (an AI agent's, a codemod's, or
your own) for a `SAFE` / `UNSAFE` / `UNPROVEN` verdict, and expose the same
gate to agents over MCP. Purely additive: nothing was renamed or removed, and
the transform CLI behaves exactly as it did in 0.2.4. Install with
`npm install -g refactron@0.3.0`.
**Added**
* **`verify-diff` command**: verify an arbitrary unified diff end to end and
print `[SAFE|UNSAFE|UNPROVEN] `. Applies the change in an isolated
shadow tree, runs the syntax / imports / tests gates, and fuses changed-line
coverage into the verdict. Read-only: your working tree is never mutated.
(PR #75)
* **`refactron-mcp`, an MCP server exposing `verify_change`**: a stdio server
an AI agent calls before it lands a change; accepts full-file `edits` or a
`unifiedDiff` and returns the same JSON verdict report. Ships as a second bin
in the same package. (PR #75)
* **Three-way verdict fusion**: `SAFE` (gates pass and the changed lines are
exercised), `UNSAFE` (a gate failed), `UNPROVEN` (tests pass but the change
isn't proven, or coverage couldn't be measured). Coverage is Python-only via
`coverage.py`; a non-Python or mixed diff returns `UNPROVEN`, never a false
`SAFE`. (PR #75)
* **`preflight` command**: a coverage-aware SQLAlchemy 1.x to 2.0 migration
safety report that classifies each `Model.query` site as safe-to-automate,
unproven, or needs-review; `--fail-on-unproven` gates CI. (PR #74)
* **A versioned report shape**: `reportVersion`, plus `changedStatements`,
`filesWithUncovered`, `testFilesChanged`, and `flakyTests`, so the evidence
behind a verdict is legible in `--json` and over MCP rather than implied.
(PRs #79, #83, #86)
**Fixed**
Each item is a false-verdict class the shipped release does not have. None
reached a published build, since `verify-diff` is new here; they are listed
because what a verification tool refuses to claim is the product.
* **A diff that deleted a file could read `SAFE`.** Unmodelable operations were
silently skipped, so a diff removing a module plus one benign edit passed the
gates. Deletions, renames, copies, and binary changes are now refused with
exit `2`. So are anchorless hunks that misrepresent a live file as new,
submodule pointer bumps, and non-UTF-8 bases. (PRs #79, #82)
* **A changed blank line could vouch for a function that never ran.** Coverage
attribution walked back to the nearest preceding statement start, so an
executed `def` header covered a body that was never called. Attribution is
now exact line-to-statement containment from the Python AST, and a line
carrying no code token never marks a file exercised. (PR #86)
* **A flaky suite became a false `UNSAFE` rate.** The tests gate now computes
new failures against the baseline set and retries once on a **fresh** shadow
tree, so a timing flake heals but an idempotency break does not. Flakiness
floors the verdict at `UNPROVEN`: a green that needed a retry is not proof.
(PR #83)
* **The imports gate failed modern Python.** `if TYPE_CHECKING:` imports,
platform-conditional imports, and the repo's pre-existing breakage were all
blamed on the change. The gate is now delta-aware and `TYPE_CHECKING`-safe,
failing only on imports the change introduced or newly broke. (PR #78)
* **Coverage reported fake zeros.** Script-form test commands, quoted
arguments, a directory named `coverage/` on `sys.path`, dynamically compiled
code, and pip-installed projects each produced an empty covered set that read
as "not exercised". Each now reports honestly, and an unmeasurable run says
coverage could not be determined instead of guessing. (PRs #78, #80, #84)
* **Removal-only diffs read like a coverage miss.** A diff that only deletes
lines now says so: there are no added lines for coverage to attest. The
verdict stays a conservative `UNPROVEN`. (PR #81)
* **CRLF diffs could produce a false `SAFE`.** Changed-line derivation now
normalizes CRLF before the diff, so a change that only differs in line
endings no longer mismatches coverage and slips through as covered. (PR #75)
* **Coverage was unavailable in CI.** `coverage.py` is now installed into the
same `python3` the test suite spawns, so the coverage-based verdict is
exercised in the CI runner instead of degrading to `UNPROVEN`. (PR #74)
**Security**
* **Dependency advisories cleared**: `npm audit` reports 0, down from 6 (4
high). `brace-expansion` and `fast-uri` moved to patched versions in the
lockfile, with no change to any declared range.
* **The PyPI wrapper no longer runs an unpinned global npm install.**
`pip install refactron` used to shell out to `npm install -g refactron` on
first use, fetching whatever was `latest` regardless of the version you
pinned. It now detects the CLI and, if missing, prints the exact matching
command and exits non-zero. The wrapper is also relicensed to Apache-2.0 to
match the rest of the project.
Reliability and observability release. Five real fixes, one feature (tier
taxonomy), one license change. No new transforms, no API breakage; every
existing call site keeps working.
**Added**
* **Tier taxonomy on every transform** (debt / modernization / style). `analyze`
output groups findings and remediation minutes by tier: the headline "N
findings" splits into "57 debt, 102 modernization, 2,569 style" instead of
one undifferentiated count.
* **`byTier` and `minutesByTier` fields** in `analyze --json` output, with the
invariant `debt + modernization + style === totalMinutes`.
* **BY TIER section** in the boxed TUI analyze output.
**Changed**
* **License: MIT → Apache 2.0.** Same permissive freedoms; adds an explicit
patent grant from contributors. See `LICENSE`, `NOTICE`, and the FAQ.
**Fixed**
* **`run --transforms=all` silently dropped 8 transforms.** The CLI's local
list had drifted out of sync with the engine's canonical order when the
v0.2.3 catalog expansion landed. CLI now imports `TRANSFORM_ORDER` directly;
drift is pinned by a test. (#48, PR #49)
* **`--files=` was ignored on `--apply`.** The glob only narrowed the
dry-run preview; the apply path silently rewrote every matching finding.
Filter is now applied to `plan.changes` before the split, so both paths
honour it. (#50, PR #52)
* **Documenter broke files with multi-line return-type signatures.** On
`def f() -> type[Union[…]]:` shapes that span multiple lines, the inserter
latched onto the first inner line of the type subscript as if it were the
body. The walker now tracks bracket depth and skips inline Protocol stubs.
(#51, PR #52)
* **`apply` and `rollback` dropped POSIX file modes.** Both paths now
round-trip mode bits.
* **`class_to_dataclass` injected imports before `from __future__`.** New
imports now land after the `__future__` block, preserving PEP 236 ordering.
* **Silent refusals in four transform sidecars.** `pep585_generics`,
`pep604_optional_union`, `datetime_utc_alias`, `callback_to_async_await`
now emit a `precondition` record on every refusal: no more "detected, but
nothing changed" with no explanation.
* **`manual_typecheck_to_hints` was the silent sidecar Bug #3 missed.** Every
refusal path now records why; gating prevents noise from unrelated
siblings; the nested-def scan stops at function boundaries. On Ansible: 16
silent files → 0; 4 records → 87 covering all 20 affected files. (#57, PR #58)
**Known follow-ups**
* `manual_typecheck_to_hints` now records refusals but on Ansible still
rewrites 0 of 20 files. Expanding the rewriter to handle docstring + body
and dispatcher + fallthrough is tracked as #59.
* Eight new transform candidates derived from a deeper Ansible scan are filed
as #62 to #69 for v0.3 / v0.4 prioritisation.
Ten new deterministic transforms (six for Python, four for TypeScript) roughly
doubling Refactron's transform coverage. Adds the `pythonVersion` config key
so version-gated rewrites can be opted in safely.
**Added: Python**
* **`super_no_args`**: `super(ClassName, self).method(...)` → `super().method(...)`. Refuses sibling/parent class names and nested-class shadows to preserve MRO.
* **`lru_cache_to_cache`**: `@functools.lru_cache(maxsize=None)` → `@functools.cache` (≥ 3.9); also rewrites the `from functools import …` line.
* **`pep585_generics`**: `typing.List` / `Dict` / `Tuple` / `Iterable` / … → `list` / `dict` / `tuple` / `collections.abc.Iterable` / … (≥ 3.9, or `from __future__ import annotations`). Refuses files with Pydantic v1 or `get_type_hints` to avoid runtime-eval crashes.
* **`pep604_optional_union`**: `Optional[X]` → `X | None`; `Union[A, B]` → `A | B` (≥ 3.10, or `from __future__ import annotations`).
* **`datetime_utc_alias`**: `datetime.timezone.utc` → `datetime.UTC` (≥ 3.11). No `__future__` override: `UTC` is a runtime attribute.
* **`yield_from_for_loop`**: `for x in y: yield x` → `yield from y`. Refuses inside `async def` (a CPython compile-stage SyntaxError LibCST's parser does not catch).
**Added: TypeScript**
* **`indexof_to_includes`**: `arr.indexOf(x) !== -1` and friends → `arr.includes(x)`. Type-aware via ts-morph (String / Array / ReadonlyArray receivers). Gated on tsconfig target ≥ ES2016.
* **`object_assign_to_spread`**: `Object.assign({}, a, b)` → `{ ...a, ...b }`. First arg must be an object literal; refuses spread-element sources. Gated on tsconfig target ≥ ES2018.
* **`string_concat_to_template_literal`**: `"…" + x + "…"` → `` `…${x}…` ``. Refuses `any` / `unknown` / non-primitive operands. Gated on tsconfig target ≥ ES2015.
* **`vue_set_delete_to_assignment`**: `Vue.set` / `this.$set` → direct assignment; `Vue.delete` / `this.$delete` → `delete obj.k`. `.js` / `.ts` only; `.vue` SFC parsing is deferred to v0.4. Refuses `delete` in expression context (return-value semantics differ). On Vue 2 codebases this is a semantic change (`Vue.set` is required for new reactive keys); caveat ships in the suggestion text.
**Added: Configuration**
* **`pythonVersion`**: pin the Python target version (`"3.9"`, `"3.11"`, …) for the four version-gated Python transforms. Auto-detected from `pyproject.toml`'s `requires-python` when unset; falls back to refusing version-gated transforms rather than guessing.
**Changed**
* **Engine composition**: multi-transform composition is now order-stable: when several transforms touch the same file, each emits its own `FileChange` carrying the cumulative content, and the last one per path is what's written to disk. Fixes a silent-data-loss bug where only the LAST transform's rewrite survived under `run --apply` (PR #38).
Quality-of-life release for the analyze → run → document pipeline: boxed CLI
output, a real `rollback` command, and a much more efficient `document`.
**Added**
* **Bordered table output**: `analyze` renders one box per file plus boxed TRANSFORMS / BY TRANSFORM / SUMMARY blocks; `run --dry-run` matches, with a CHANGES table and a four-sided diff box per file.
* **`rollback` command**: undo an applied refactor or `document` run; journal-based LIFO undo, drift-safe, `--all` / `--force` / `--dry-run`.
* **`run --apply` live progress**: gate-by-gate status and per-file verify/apply detail; batch-first with a per-file fallback when the batch fails.
* **`run --apply` short-circuit**: exits early when no test runner is detected, instead of silently skipping the test gate.
* **Full report saved to disk**: `analyze` / `run --dry-run` write the complete report to `.refactron/reports/`.
* **`document` enrichments**: inline comments, a per-run modernization report under `docs/refactron/`, and a post-apply syntax re-check.
**Changed**
* **`document` is far more efficient**: docstring requests are batched with bounded concurrency and token-aware rate limiting; the LLM call count is now `O(source tokens / batch budget)`, not `O(symbols)`.
**Fixed**
* `document` produced zero docstrings on large files: batches were sized by input tokens only, so the combined response overran the completion cap and truncated. Batches are now also capped by response size, and a truncated reply is salvaged entry-by-entry.
* `document` six-quote docstring bug (`""""""…""""""`); rate-limited runs that ground on for minutes; report / CHANGELOG paths normalized to forward slashes on Windows.
* `analyze`: old-string-format findings now anchor on the operator, not the opening quote; `manual_typecheck_to_hints` no longer flags already-annotated parameters; the misleading "Fixable N/N" became an honest auto-fix-candidate count.
* `deprecated_api_requests_to_httpx` no longer emits runtime-broken code: it refuses files using `requests` API that is not a safe `httpx` drop-in.
Patch release: a large-file crash fix and two transform-coverage improvements.
**Fixed**
* `analyze` crashed on files larger than \~32 KB: tree-sitter's native binding rejects oversized string input. Parsing now uses the streaming callback-input form; a single unparseable file is skipped rather than aborting the run.
* `var_to_const_let` dropped whole files: reassignment checks matched identifiers by text across the entire file. Reference resolution is now scope-correct, and for-loop `var i` initializers are covered.
**Changed**
* `format_to_fstring` now converts the full printf grammar: `%d`, `%.2f`, `%x`, `%o`, `%e`, `%g`, width/precision specifiers, and `%%`. Mapping `%(name)s`, non-literal targets, and dynamic `*` widths are still conservatively skipped.
First public release of the v2.0 deterministic-refactoring rebuild.
**Added**
* **Engine**: 10 deterministic AST transforms (5 Python via LibCST, 5 TypeScript via ts-morph) with cross-file preconditions.
* **3-gate verification**: syntax + imports + tests on a shadow tree, with atomic batch write or rollback.
* **Documentation engine**: the only LLM-touching component, running only on already-verified diffs; 5 providers (Ollama, Groq, OpenAI, Anthropic, managed backend).
* **`.refactronrc.json` config**: cosmiconfig + Ajv schema validation.
* **Authentication**: OAuth device flow with `REFACTRON_TOKEN` support and long-lived API keys.
* A Mintlify documentation site and reproducible performance benchmarks under `bench/`.
# Safety Model
Source: https://docs.refactron.dev/concepts/safety-model
Three gates and a coverage check, run against your change in an isolated shadow tree. The engine that turns a diff into a SAFE, UNSAFE, or UNPROVEN verdict.
Refactron's value is what happens **before** you trust a change, not how clever any rewrite is. Every change you [verify](/verification/verify-diff) passes through the same engine: an isolated shadow tree, three gates, and a coverage check that fuses into one verdict.
> **Inviolable rule:** verification runs against a copy, never your working tree. Refactron never writes to it.
## The shadow tree
Nothing in the gate path reads or writes your real working tree. Refactron builds a **shadow tree** first: a temp-directory copy of the project root where unchanged files are hardlinked (cheap, instant) and the changed files are written from the proposed new content.
For [`verify-diff`](/verification/verify-diff) and the [MCP `verify_change` tool](/mcp/overview), this is the whole story: the shadow tree is built, the gates run, the verdict is returned, and the copy is cleaned up. **Your repo is never mutated.** Landing the change is the caller's decision.
## The three gates
```mermaid theme={null}
flowchart LR
Plan[Change] --> G1{Gate 1
Syntax}
G1 -- pass --> G2{Gate 2
Imports}
G2 -- pass --> G3{Gate 3
Tests}
G3 -- pass --> COV[Coverage check]
G1 -- fail --> Reject[UNSAFE, tree untouched]
G2 -- fail --> Reject
G3 -- fail --> Reject
```
### Gate 1: Syntax
Re-parse the new content for each changed file.
* **Python:** LibCST parses the proposed source. A failure means a malformed change slipped through.
* **TypeScript:** ts-morph collects diagnostics; any `Error`-category diagnostic rejects the file.
If any file fails to parse, the gate rejects with a `blockingReason` naming the offending file. Typical wall-clock: \~50ms for a small change.
### Gate 2: Imports
Resolve every import in the changed files, and reject only the ones the change is actually responsible for.
* **Python:** collect `import X` and `from X import Y` and resolve each top-level module against `sys.path` plus the project tree. Imports guarded by `if TYPE_CHECKING:` are skipped, because they never run at runtime; a type-only dependency that is not installed is not a runtime break. Relative imports are left to the tests gate.
* **TypeScript:** resolve every import specifier against the project's `tsconfig.json`. Node builtins always count as resolved.
**Delta-aware.** The gate resolves imports in both the base file (your real tree) and the changed file (the shadow tree), then fails only on imports that are unresolvable in the changed file but were fine in the base file. An import that was already broken before your change is the repo's pre-existing state, not something the change introduced, so it never fails the gate. A brand-new file has an empty baseline, so every unresolvable import in it counts.
**Reject conditions:**
* The change adds an import that does not resolve.
* The change breaks an import that resolved before (for example, it deleted a re-exported symbol a changed file relied on).
One accepted limitation: an import you newly add that is guarded by a platform condition (say `import msvcrt`, which exists only on Windows) can still fail when the gate runs on another OS. The gate does not evaluate platform conditions, and stays conservative there.
This catches the most common breakage: the change is locally valid but quietly orphans a downstream module, without blaming your edit for imports the repo could never resolve in the first place. Typical wall-clock: \~50ms.
### Gate 3: Tests
The most expensive gate, and the only one that runs **your** code.
1. **Detect the test runner** by config-file *presence* in the project root (no file contents are parsed):
* `vitest.config.ts` or `vitest.config.js` → `vitest run`
* `jest.config.js` or `jest.config.ts` → `jest`
* any of `pyproject.toml`, `pytest.ini`, or `setup.cfg` present → `pytest`
* Override with `--test-cmd` (CLI) or the `testCmd` argument (MCP).
2. **Run the baseline first**: the runner against the *unchanged* copy. If the baseline already fails, Refactron won't blame the change: the tests gate reports an already-red baseline, and the verdict becomes [`UNPROVEN`](/verification/verdicts), not `UNSAFE`.
3. **Run the mutated tree**: same runner, with the change in place. A non-zero exit fails the gate.
The test-gate timeout defaults to 600 seconds (10 minutes); wall-clock is otherwise dominated by your own suite. (Refactron's legacy blast-radius engine scaled this timeout by a change's reach; the `verify-diff` and MCP path applies the flat default.)
## Coverage fusion → the verdict
Passing the gates is necessary but not sufficient. A green suite says nothing about lines your tests never run. So when the gates pass, Refactron measures **changed-line coverage** and fuses the two signals into one verdict:
| Gates | Changed lines exercised? | Verdict |
| ------------- | ------------------------ | ---------- |
| A gate failed | n/a | `UNSAFE` |
| All passed | yes | `SAFE` |
| All passed | no, or couldn't tell | `UNPROVEN` |
Coverage is measured with `coverage.py`, so it is **Python-only** today. A non-Python or mixed diff passes the gates but returns `UNPROVEN` ("coverage could not be determined"); Refactron never reports an unmeasured change as `SAFE`. The full rules, including the `missingTests` hints, are on the [Verdicts](/verification/verdicts) page.
## Atomic batch write (landing)
`verify-diff` never writes, and as of 0.4.0 nothing in Refactron does. Earlier versions had a migration mode that wrote on green, handing a list of `(path, newContent)` pairs to an atomic batch writer so a partial failure could never leave a half-written tree. That path was removed with the transforms; the guarantee it protected is now unconditional.
## Citations
The gate-before-transform model traces back to Bill Opdyke's 1992 PhD thesis on behaviour-preserving refactoring at UIUC: the original formal treatment of preconditions before automated source transformation.
* Opdyke, William F. *Refactoring Object-Oriented Frameworks.* PhD thesis, University of Illinois at Urbana-Champaign, 1992. [PDF](https://www.cs.umd.edu/users/atif/Refactoring.pdf)
# Deterministic by design
Source: https://docs.refactron.dev/concepts/why-no-llm
Generating a change is probabilistic. Deciding whether it's safe has to be deterministic. Refactron is the gate an LLM agent calls to prove its own work.
Refactron is **the deterministic gate LLM agents call before they land a change.** The agent proposes; Refactron proves. That division of labor is the whole design.
## Two different jobs
Writing code and *certifying that a change is safe* are different problems, and they want different tools.
* **Generating a change is a good fit for a probabilistic model.** An LLM explores a huge space of plausible edits and usually lands on a reasonable one. Different runs can produce different code, and that's fine; you're going to check it.
* **Deciding whether that change preserved behavior is a formal problem.** The answer must be reproducible: the same repo state and the same diff have to yield the same verdict, every time. A judgment you can't reproduce isn't proof; it's another opinion.
Put a probabilistic proposer behind a deterministic verifier and you get the best of both: fast generation, and an answer you can stand behind.
## What "deterministic" buys you
Refactron's verification engine (the [shadow tree, the three gates, and the coverage check](/concepts/safety-model)) contains no model, no temperature, no sampling. Run a [verify](/verification/verify-diff) twice on the same inputs and you get the identical [verdict](/verification/verdicts) and the identical report.
That reproducibility is what makes the verdict *usable*:
* **An agent can act on it.** A `SAFE` / `UNSAFE` / `UNPROVEN` answer that flips between runs is worthless as a gate. A stable one is a decision an agent, or a CI job, can branch on.
* **A human can audit it.** "The tests passed and these exact lines ran" is a claim you can re-check. "The model thought it looked fine" is not.
* **It stays honest.** When the evidence isn't there, a deterministic gate returns [`UNPROVEN`](/verification/verdicts) instead of guessing. It never rounds "I'm not sure" up to "looks good."
## Where a model does fit
The verdict is never a model's call, and as of 0.4.0 there is no model anywhere in Refactron. Earlier versions shipped an optional `document` command that generated docstrings and changelog prose on top of an already-verified diff; it was removed with migration mode. Even then the rule held: a model was welcome where fluent language was the goal, and kept out of the path that decides whether a change is safe.
That's the rule the whole product follows: models are welcome where fluent language is the goal, and kept out of the path that decides whether your change is safe.
## The takeaway
You don't have to trust Refactron the way you trust a model. You can re-run it and get the same answer, read exactly which gates passed and which lines your tests exercised, and see the reasoning behind every verdict. Determinism isn't a limitation here; it's the feature that lets an agent hand off the one job it's worst at.
## Citations
The idea that a transformation is only safe once explicit preconditions hold (the conceptual root of a deterministic gate) comes from Bill Opdyke's 1992 thesis.
* Opdyke, William F. *Refactoring Object-Oriented Frameworks.* PhD thesis, UIUC, 1992. [PDF](https://www.cs.umd.edu/users/atif/Refactoring.pdf)
# FAQ
Source: https://docs.refactron.dev/faq
Frequently-asked questions about Refactron: the verification layer for AI code change.
## What does Refactron actually do?
It verifies a change. You give it a diff (from your AI agent, a codemod, or your own editor) and it proves whether the change preserved behavior. It applies the change in an isolated [shadow tree](/concepts/safety-model), runs the syntax, imports, and test gates, checks whether your tests exercise the changed lines, and returns one of three [verdicts](/verification/verdicts): `SAFE`, `UNSAFE`, or `UNPROVEN`. Your working tree is never touched.
***
## What is UNPROVEN?
It's the honest verdict, and the reason to trust the other two. `UNPROVEN` means the gates passed (nothing is known to be broken), but the change isn't *proven* safe, either because no test exercises the changed lines, or because coverage couldn't be measured.
"Tests pass" is not the same as "this change is proven safe." If nothing runs the lines you changed, a green suite tells you nothing about them. Most tools quietly treat green as good enough. Refactron says `UNPROVEN` and, when it can, names the exact line to add a test for. `UNPROVEN` exits `0`; it's a warning, not a rejection. See [Verdicts](/verification/verdicts).
***
## Does it work with my AI agent?
Yes, that's the point. Refactron ships an [MCP server](/mcp/overview) with a `verify_change` tool. Your agent proposes an edit, calls `verify_change`, and gets back the `SAFE` / `UNSAFE` / `UNPROVEN` report before it lands anything. Refactron doesn't replace Cursor, Claude Code, Copilot, or Codex; it's the deterministic gate they call to check their own work.
***
## Does Refactron modify my repo?
`verify-diff` and the MCP `verify_change` tool are **read-only**. They copy your project into a shadow tree, apply the change *there*, run the tests, and clean up. Your working tree is never mutated; landing the change is your (or your agent's) decision.
Nothing in Refactron writes to your project. Migration mode, which did write (on green, atomically), was removed in 0.4.0. See the [safety model](/concepts/safety-model).
***
## Why does my TypeScript change never come back SAFE?
Because coverage is **Python-only** today. Reaching `SAFE` requires measuring that your tests exercise the changed lines, and Refactron does that with `coverage.py`. A change that touches any TypeScript (or any non-Python) file, or one run without `coverage.py`, passes the gates but returns `UNPROVEN` with the reason "coverage of the changed code could not be determined." The gates still protect you; only the coverage half is Python-only. Refactron never reports an unmeasured change as `SAFE`; that would be a false `SAFE`.
***
## Is it free?
The CLI, the MCP server, and single-repo verification are free and open source (Apache 2.0); that's everything on this docs site today.
Paid tiers for **fleet verification across many repos** and **audit history** are on the roadmap, not shipping yet. The line is deliberate: verifying one change is free forever; managing trust across a fleet over time is the paid layer.
***
## Doesn't running my tests in CI already do this?
Running your suite tells you whether *the suite* is green. Refactron tells you whether *this specific change* is proven. Three things CI doesn't give you out of the box:
* **Coverage fusion.** A green suite that never touches your changed lines earns `UNPROVEN`, not a false pass.
* **A reproducible report.** The same repo state and diff produce the same verdict and the same JSON record every time, something you can re-run and audit.
* **An isolated shadow tree.** The change is tested against a copy, so a broken change never sits in your working tree.
***
## Does it work on monorepos?
Yes. The test gate runs in the package being changed; it detects the local runner via config-file presence (`vitest.config.*`, `jest.config.*`, `pyproject.toml` / `pytest.ini`), and you can override with `--test-cmd`. The import-graph checks scan from the changed file, which works across package boundaries inside the same repo.
***
## Why no Rust / Go / Ruby support?
Each language needs its own syntax and imports checks, and — to reach `SAFE` rather than `UNPROVEN` — a coverage tool we can run exactly as your test command runs it. Python and TypeScript came first: TypeScript reaches the JavaScript world, Python covers the data / scripting / ML surface, and Python's `coverage.py` is what powers the `SAFE` verdict today. If you'd use Refactron in a Go monorepo, [open an issue](https://github.com/Refactron-ai/refactron/issues); concrete demand moves it up the list.
***
## What happened to the transforms, and to `analyze` and `run`?
They were removed in 0.4.0. Up to 0.3.x this package also shipped a migration
mode: 20 deterministic AST transforms that both *authored* a mechanical change
and *verified* it through these same gates before an atomic write, driven by the
`analyze`, `run` and `document` commands.
They were the demo of the verification engine, not the product, and carrying them
made the package twice the size of the thing people actually install it for.
The code is archived with its full history and is not currently published. If you
depend on it, pin `refactron@0.3.1`.
***
## Why Apache 2.0?
Apache 2.0 gives the same permissive freedoms as MIT plus an **explicit patent grant** from every contributor. For a tool that touches source code, that removes ambiguity about patent exposure: the bar enterprise legal teams ask about. We'd rather answer that once than repeatedly.
***
## Why 0.3 and not 1.0?
The engine has had limited external exercise. 0.3.0 is the first release to publish the verification surfaces, so their real-world bug patterns are still ahead of us. 1.0 lands after that usage has characterized them and we've addressed them.
# Claude Code
Source: https://docs.refactron.dev/mcp/claude-code
Register the Refactron MCP server with Claude Code using the claude mcp add command or a project .mcp.json.
Claude Code has a built-in command for this, so you do not have to edit a file by hand.
## Set it up with your agent
One prompt, written for Claude Code to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Claude Code.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Run:
claude mcp add refactron -- refactron-mcp
The `--` separator is required. Use `--scope project` instead if I asked for a
config the whole team gets; that writes `.mcp.json` in the repository root.
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
claude mcp add refactron -- npx -y -p refactron refactron-mcp
STEP 4 - Reload.
Claude Code picks the server up on the next session. If you are mid-session and
it does not appear in step 5, tell me to restart `claude`.
A project-scoped `.mcp.json` also needs a one-time approval. If step 5 shows
"Pending approval", tell me to start `claude` in this directory and approve the
server once.
STEP 5 - Confirm the tool is exposed.
Run:
claude mcp list
`refactron` should show as Connected. `claude mcp get refactron` prints the
full record, including scope and resolved command. Inside a session, `/mcp`
lists connected servers and their tools.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## Config location
| Scope | Where it is written | Who sees it |
| --------- | -------------------------------------------------------------- | -------------------------------- |
| `local` | `~/.claude.json`, under the current project path (the default) | You, in this project only |
| `user` | `~/.claude.json`, at the top level | You, in every project |
| `project` | `.mcp.json` in the repository root | Everyone who checks out the repo |
## Add it
```bash theme={null}
claude mcp add refactron -- refactron-mcp
```
```text theme={null}
Added stdio MCP server refactron with command: refactron-mcp to local config
```
To share the server with your team, commit it at project scope:
```bash theme={null}
claude mcp add --scope project refactron -- refactron-mcp
```
That writes `.mcp.json` in the repository root:
```json theme={null}
{
"mcpServers": {
"refactron": {
"type": "stdio",
"command": "refactron-mcp",
"args": [],
"env": {}
}
}
}
```
To pin the version without a global install, register the `npx` form instead:
```bash theme={null}
claude mcp add refactron -- npx -y -p refactron refactron-mcp
```
## Verify the connection
```bash theme={null}
claude mcp list
```
```text theme={null}
refactron: refactron-mcp - ✔ Connected
```
For the full record, including scope and resolved command:
```bash theme={null}
claude mcp get refactron
```
Inside a session, `/mcp` lists connected servers and their tools. You are looking for one tool, `verify_change`, under `refactron`.
## Troubleshooting
* **`⏸ Pending approval (run 'claude' to approve)`**: a project-scoped `.mcp.json` needs your approval before it runs. Start `claude` in that directory and approve the server once.
* **`✘ Failed to connect`**: the command is not on your `PATH`. Run `which refactron-mcp`. If it prints nothing, run `npm install -g refactron`, or re-register with the `npx` form above.
* **The server connects but no tool appears**: the handshake failed. Send one `initialize` request straight to the binary as in the [MCP quickstart](/mcp/quickstart#2-smoke-test-the-server-without-a-client) to see whether the server or the client is at fault.
* **Registered in the wrong project**: local scope is keyed to the directory you ran `claude mcp add` in. Run `claude mcp list` from the project you meant, and re-add there or use `--scope user`.
* **Removing it**: `claude mcp remove refactron`, adding `-s project` or `-s user` if you did not use the default scope.
# Claude Desktop
Source: https://docs.refactron.dev/mcp/claude-desktop
Add the Refactron MCP server to Claude Desktop by editing claude_desktop_config.json, then restart the app.
Claude Desktop reads one JSON file at launch. There is no CLI, and changes take effect only after a full restart.
## Set it up with your agent
One prompt, written for Claude Desktop to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Claude Desktop.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Claude Desktop has no CLI. Edit its config file, root key "mcpServers":
macOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json
Linux ~/.config/Claude/claude_desktop_config.json
Create the file if it is missing. If it already has servers, add "refactron" as
another key inside the existing "mcpServers" object rather than adding a second
"mcpServers" block:
{ "mcpServers": { "refactron": { "command": "refactron-mcp" } } }
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "mcpServers": { "refactron": { "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"] } } }
Validate the JSON before you finish. Claude Desktop skips a malformed config
silently, so a trailing comma looks exactly like a broken install.
Claude Desktop launches from the desktop environment, not from your shell, so
it does not inherit PATH additions made in .zshrc or .bashrc. If step 5 reports
`spawn refactron-mcp ENOENT`, put the absolute path from `which refactron-mcp`
in "command" instead.
STEP 4 - Reload.
Claude Desktop reads that file once at launch, and it needs a FULL quit and
reopen. Closing the window is not enough on macOS.
You cannot do this yourself. Tell me to press Cmd+Q, or quit from the Claude
menu, and reopen the app. Wait for me before you continue.
STEP 5 - Confirm the tool is exposed.
Ask me to open Settings, then Developer, and read you the server list.
`refactron` should be listed with a `running` status. In a chat, the tools
control below the message box lists the connected servers.
If it failed to start, the logs are per-server: on macOS
~/Library/Logs/Claude/mcp-server-refactron.log, on Windows
%APPDATA%\Claude\logs\.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## Config location
| OS | Path |
| ------- | ----------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
The app opens this file for you: **Settings** then **Developer** then **Edit Config**. If the file does not exist yet, create it with the contents below.
## Add it
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
If you already have servers configured, add `refactron` as another key inside the existing `mcpServers` object rather than adding a second `mcpServers` block.
To pin the version and skip the global install:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"]
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
Quit Claude Desktop completely and reopen it. Closing the window is not enough on macOS: use **Claude** then **Quit**, or `Cmd+Q`.
## Verify the connection
Open **Settings** then **Developer**. `refactron` appears in the server list with a `running` status.
In a chat, the tools control below the message box lists the connected servers. Under `refactron` you should see exactly one tool: `verify_change`.
## Troubleshooting
* **The server does not appear at all**: the JSON is invalid. Claude Desktop skips a malformed config silently. Check it with `node -e "JSON.parse(require('fs').readFileSync('','utf8'))"` and look for a trailing comma.
* **`spawn refactron-mcp ENOENT` in the logs**: Claude Desktop launches from the desktop environment, not your shell, so it does not inherit `PATH` additions made in `.zshrc` or `.bashrc`. Use the absolute path from `which refactron-mcp` as the `command` value.
* **Reading the logs**: macOS writes them to `~/Library/Logs/Claude/`, one `mcp-server-refactron.log` per server. Windows uses `%APPDATA%\Claude\logs\`.
* **Edits appear to do nothing**: the config is read once at launch. Quit the app fully and reopen.
* **A verification times out**: `verify_change` runs your whole test suite, which can take minutes. That is the work, not a hang.
# Codex CLI
Source: https://docs.refactron.dev/mcp/codex
Add the Refactron MCP server to OpenAI Codex CLI. Note the format: Codex reads TOML, not JSON.
Codex CLI is the one client on this list that does not use JSON. Its MCP servers live in `~/.codex/config.toml`, under a `[mcp_servers.]` table. A config block copied from Claude Desktop or Cursor is not valid here.
## Set it up with your agent
One prompt, written for Codex CLI to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Codex CLI.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Run:
codex mcp add refactron -- refactron-mcp
The `--` separator is required. Everything after it is the command Codex
launches; everything before it is Codex's own flags.
Codex uses TOML, not JSON. If you edit ~/.codex/config.toml by hand, the entry
is a table, and the prefix is `mcp_servers` with an underscore, not
`mcpServers`:
[mcp_servers.refactron]
command = "refactron-mcp"
args = []
tool_timeout_sec = 900
Set `tool_timeout_sec` either way, editing the table afterwards if you used
`codex mcp add`. Codex's default per-tool timeout has changed between releases,
and either value can be shorter than a real suite run. When it fires you get a
tool failure rather than the verdict you already paid for.
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
[mcp_servers.refactron]
command = "npx"
args = ["-y", "-p", "refactron", "refactron-mcp"]
tool_timeout_sec = 900
Check for a duplicate [mcp_servers.refactron] table before you finish. Two of
them is a parse error, and Codex rejects the whole config.
STEP 4 - Reload.
Codex CLI picks the server up on the next session. If it does not appear in
step 5, tell me to restart `codex`.
STEP 5 - Confirm the tool is exposed.
Run:
codex mcp list
`refactron` appears with its launch command. `codex mcp get refactron` prints
the resolved entry, and both accept --json. Then start a session and ask for
the tool list.
If it is configured but never starts, check for `enabled = false` on the table.
That key disables a server without removing it.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## The format is TOML, not JSON
Every other client on this list takes a JSON object keyed by server name. Codex takes a TOML table per server, and the table prefix is `mcp_servers` with an underscore, not `mcpServers`.
## Config location
| Scope | Path | Applies to |
| ------- | ------------------------------------------- | ------------------------------- |
| User | `~/.codex/config.toml` | Every project |
| Project | `.codex/config.toml` in the repository root | That project, once you trust it |
## Add it
The `codex mcp add` subcommand writes the entry for you and gets the TOML shape right:
```bash theme={null}
codex mcp add refactron -- refactron-mcp
```
The `--` separator is required. Everything after it is the command Codex launches; everything before it is Codex's own flags.
To write it by hand, add this table to `~/.codex/config.toml`:
```toml theme={null}
[mcp_servers.refactron]
command = "refactron-mcp"
args = []
tool_timeout_sec = 900
```
To pin the version and skip the global install:
```toml theme={null}
[mcp_servers.refactron]
command = "npx"
args = ["-y", "-p", "refactron", "refactron-mcp"]
tool_timeout_sec = 900
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
## Set `tool_timeout_sec` yourself
Codex applies a per-tool timeout, and its default has changed between releases: the published config reference documents 60 seconds, while current builds use a longer one. Either number can be shorter than a real test suite, and `verify_change` runs your whole suite.
Set `tool_timeout_sec` explicitly to comfortably more than a full suite run. Without it, a slow project reports a tool failure that is really a client-side timeout, and you lose a verdict you had already paid for.
`startup_timeout_sec` is a separate key covering only process launch and the MCP handshake. The default is fine for this server, which starts in well under a second.
## Verify the connection
```bash theme={null}
codex mcp list
```
`refactron` appears in the list with its launch command. For the single entry, including the resolved config:
```bash theme={null}
codex mcp get refactron
```
Both accept `--json` if you want to read the entry from a script.
Then start a session and ask for the tool list. You are looking for one tool, `verify_change`, under `refactron`.
## Troubleshooting
* **The table name is wrong**: it is `[mcp_servers.refactron]`. A `[mcpServers.refactron]` table is silently a different, unused key. Codex does not warn you.
* **The server is configured but never starts**: check for `enabled = false` on the table. That key disables a server without removing it, and it is easy to leave behind.
* **`command not found` on launch**: run `which refactron-mcp`. If it prints nothing, run `npm install -g refactron`, or switch to the `npx` form above.
* **A project-scoped `.codex/config.toml` is ignored**: Codex reads project config only for a trusted project. Trust the directory, or move the entry to `~/.codex/config.toml`.
* **A verification fails after about a minute on a suite that takes longer**: that is `tool_timeout_sec`, not a verdict. Raise it.
* **Duplicate TOML tables**: two `[mcp_servers.refactron]` tables in one file is a parse error, and Codex rejects the whole config. If you ran `codex mcp add` after hand-editing, check for both.
## Remove it
```bash theme={null}
codex mcp remove refactron
```
# Cursor
Source: https://docs.refactron.dev/mcp/cursor
Add the Refactron MCP server to Cursor with a project .cursor/mcp.json or the global ~/.cursor/mcp.json.
Cursor reads MCP servers from a project file or a global one. The project file is the better default: it travels with the repository, so everyone on the team gets the same gate.
## Set it up with your agent
One prompt, written for Cursor to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Cursor.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Create `.cursor/mcp.json` in the repository root, root key "mcpServers". The
project file is the better default: it travels with the repository, so the
whole team gets the same gate. The global file is ~/.cursor/mcp.json.
{ "mcpServers": { "refactron": { "command": "refactron-mcp" } } }
Merge into the existing "mcpServers" object if the file already has servers.
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "mcpServers": { "refactron": { "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"] } } }
STEP 4 - Reload.
Cursor reloads MCP servers on a toggle, not on file save.
You cannot do this yourself. Tell me to open Cursor Settings, then Tools &
Integrations, called MCP in older builds, and switch the `refactron` toggle off
and back on. Wait for me before you continue.
STEP 5 - Confirm the tool is exposed.
Ask me to confirm that `refactron` shows a green indicator in Cursor Settings
under Tools & Integrations, with one tool beneath it.
Tools are only available in Agent mode. Ask mode does not call tools at all, so
if you cannot see the tool, check the mode before you suspect the config.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## Config location
| Scope | Path | Applies to |
| ------- | ------------------------------------------------------- | ---------------------- |
| Project | `.cursor/mcp.json` in the repository root | This project only |
| Global | `~/.cursor/mcp.json` (`%USERPROFILE%\.cursor\mcp.json`) | Every project you open |
## Add it
Create the file if it does not exist:
```bash theme={null}
mkdir -p .cursor
```
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
To pin the version and skip the global install:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"]
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
## Verify the connection
Open **Cursor Settings** then **Tools & Integrations** (called **MCP** in older builds). `refactron` appears in the list with a green indicator and one tool beneath it: `verify_change`.
Then ask the agent in a Chat pane, with Agent mode selected:
```text theme={null}
List the tools you have from the refactron MCP server.
```
It should name `verify_change` and nothing else.
## Troubleshooting
* **The server shows red or `Client closed`**: Cursor could not launch the command. Run `which refactron-mcp`. If it prints nothing, run `npm install -g refactron`, or switch to the `npx` form above.
* **Nothing appears after saving**: Cursor reloads servers on a toggle, not on file save. Switch the `refactron` toggle off and on in the settings pane.
* **The tool exists but the agent never calls it**: tools are only available in Agent mode. Ask mode does not call tools. See [Make it verify, not just connect](/mcp/overview#make-it-verify-not-just-connect) for a rule that makes the agent verify on its own.
* **`repoRoot` errors**: the tool needs an absolute path. Tell the agent to pass the project's absolute path, not `.` and not a path relative to anything.
* **A verification takes minutes**: `verify_change` runs your real test suite in a shadow copy. Long is expected on a large suite.
# Gemini CLI
Source: https://docs.refactron.dev/mcp/gemini-cli
Add the Refactron MCP server to Gemini CLI with the gemini mcp add command or ~/.gemini/settings.json.
Gemini CLI reads MCP servers from `settings.json` under the root key `mcpServers`, the same shape Claude Desktop and Cursor use. There is a subcommand that writes the entry for you.
## Set it up with your agent
One prompt, written for Gemini CLI to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Gemini CLI.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Run:
gemini mcp add refactron refactron-mcp
That defaults to PROJECT scope and writes `.gemini/settings.json` in whatever
directory you run it from. Add `--scope user` for ~/.gemini/settings.json
instead.
By hand, the root key is "mcpServers" and `timeout` is in MILLISECONDS:
{ "mcpServers": { "refactron": { "command": "refactron-mcp",
"timeout": 900000 } } }
900 would mean 0.9 seconds and every call would fail instantly. Gemini CLI
defaults to 600000, the same order as Refactron's own 600 second test gate, so
set it above the slowest full run of this suite.
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "mcpServers": { "refactron": { "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"],
"timeout": 900000 } } }
STEP 4 - Reload.
Gemini CLI picks the server up on the next session. If it does not appear in
step 5, tell me to restart `gemini`.
STEP 5 - Confirm the tool is exposed.
Run:
gemini mcp list
`refactron` should show as Connected. Inside a session, `/mcp` shows connected
servers with their tools expanded.
If it shows Disconnected and the command is right, the folder is not trusted. A
stdio server only starts in a trusted folder, so tell me to run `gemini trust`
here. This is the failure that looks like a broken install and is not one.
If it is configured but never connects, check the top-level `mcp` object for an
`allowed` list that does not name `refactron`.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## Config location
| Scope | Path | Applies to |
| ------- | ---------------------------------------------- | ----------------- |
| User | `~/.gemini/settings.json` | Every project |
| Project | `.gemini/settings.json` in the repository root | This project only |
`gemini mcp add` defaults to **project** scope, so it writes `.gemini/settings.json` unless you ask otherwise.
## Add it
```bash theme={null}
gemini mcp add refactron refactron-mcp
```
That writes the project file. For every project on the machine, use user scope:
```bash theme={null}
gemini mcp add --scope user refactron refactron-mcp
```
By hand, add this to `settings.json` alongside the objects already there:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp",
"timeout": 900000
}
}
}
```
`timeout` is in **milliseconds** here, not seconds. Gemini CLI defaults to 600000 (10 minutes), which is the same order as Refactron's own 600 second test-gate default, so a suite that runs long enough to hit one will be near the other. Set it above your slowest full suite run.
To pin the version and skip the global install:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"],
"timeout": 900000
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
## Verify the connection
```bash theme={null}
gemini mcp list
```
```text theme={null}
✓ refactron: command: refactron-mcp (stdio) - Connected
```
Inside a session, the `/mcp` slash command shows connected servers with their tools expanded. You are looking for one tool, `verify_change`, under `refactron`.
## Troubleshooting
* **The server shows `Disconnected` and the command is right**: Gemini CLI only starts a stdio server in a trusted folder. Run `gemini trust` in the project directory, then list again. This is the failure that looks like a broken install and is not one.
* **`gemini mcp add` put it somewhere unexpected**: the default scope is project, so the entry landed in `.gemini/settings.json` in whatever directory you ran it from. Re-run with `--scope user` for a machine-wide entry.
* **`command not found` on launch**: run `which refactron-mcp`. If it prints nothing, run `npm install -g refactron`, or switch to the `npx` form above.
* **A verification is cut off partway**: `timeout` is milliseconds. `900` means 0.9 seconds, not 15 minutes, and every call fails instantly.
* **Confirmation prompts on every call**: set `"trust": true` on the `refactron` entry to skip them. The server reads your repository and writes only to a temporary shadow copy, so this is a reasonable server to trust. It is a per-server setting, so it does not loosen anything else.
* **The server is configured but excluded**: check the top-level `mcp` object for an `allowed` list. If `mcp.allowed` is set and does not name `refactron`, the CLI never connects to it.
# Any other client
Source: https://docs.refactron.dev/mcp/other-clients
Connect Refactron to any MCP client that can launch a stdio server, plus the root keys and config shapes each client family expects.
Refactron is an ordinary stdio MCP server. Any client that can launch a subprocess and speak JSON-RPC over its standard streams can run it.
## Set it up with your agent
One prompt, written for an MCP client that this documentation does not cover by name to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in an MCP client that this documentation does not cover by name.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
First work out which client you are running in, from your own runtime, not
from anything in this repository. If you genuinely cannot tell, ask me before
you write any file.
Refactron is an ordinary stdio MCP server: command `refactron-mcp`, no
arguments, no environment, no API key. Only the root key differs by client:
Claude Code, Claude Desktop, Cursor, Windsurf, Gemini CLI, Cline
JSON, root key "mcpServers"
VS Code with Copilot JSON, root key "servers", plus "type": "stdio"
Zed JSON, top-level key "context_servers"
Codex CLI TOML, table [mcp_servers.refactron]
The server object, for every client that takes JSON:
{ "refactron": { "command": "refactron-mcp" } }
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "refactron": { "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"] } }
Merge into any existing config rather than overwriting it.
Set a tool timeout if your client has one, and check the unit: Cline and Codex
CLI count seconds, Gemini CLI counts milliseconds. A verification runs the whole
test suite, so any default under a minute cuts it off and reports a failure
that is really a timeout.
STEP 4 - Reload.
Reload however your client requires: a new session, a full application restart,
a settings toggle, or a refresh button.
If you cannot trigger it yourself, say so plainly and tell me exactly what to
click. Do not claim the setup is done before the server is connected.
STEP 5 - Confirm the tool is exposed.
Use your client's own command or panel where one exists: `claude mcp list`,
`codex mcp list`, `gemini mcp list`, or the MCP panel in an editor.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## What every client needs to know
| Setting | Value |
| ----------- | -------------------- |
| Transport | stdio |
| Command | `refactron-mcp` |
| Arguments | none |
| Environment | none required |
| Server name | `refactron` |
| Version | `0.3.0` |
| Tools | one, `verify_change` |
There is no HTTP or SSE endpoint, no API key, and no network call. The server reads your repository, runs your tests in a temporary shadow copy, and writes the verdict back over stdout.
## The root key differs by client
This is where most setups fail. The block is otherwise identical.
| Client family | Format | Root key |
| ---------------------------------------------------------------- | ------ | ----------------- |
| Claude Code, Claude Desktop, Cursor, Windsurf, Gemini CLI, Cline | JSON | `mcpServers` |
| VS Code with GitHub Copilot | JSON | `servers` |
| Zed | JSON | `context_servers` |
| Codex CLI | TOML | `[mcp_servers.*]` |
The `mcpServers` form, which most clients accept:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
## Without a global install
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"]
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own, so `npx refactron-mcp` looks up a package by that name and fails with a registry 404 unless a global install already put the binary on your `PATH`.
## Clients without a page of their own
### Zed
Zed calls MCP servers "context servers" and configures them in its main `settings.json`, at the top-level key `context_servers`. A block copied from Claude Desktop or Cursor does nothing here, with no error.
| OS | Path |
| ------------ | ----------------------------- |
| macOS, Linux | `~/.config/zed/settings.json` |
| Windows | `%APPDATA%\Zed\settings.json` |
```json theme={null}
{
"context_servers": {
"refactron": {
"command": "refactron-mcp",
"args": [],
"env": {}
}
}
}
```
Zed restarts the context server process when you save `settings.json`, so no editor restart is needed. Some builds also expect `"source": "custom"` on a hand-written entry. If the server does not appear after saving, add it through **Agent Panel** then **Settings** then **Add Custom Server**, which writes the shape your build expects.
### Cline
Cline manages servers from its own panel inside VS Code, backed by `cline_mcp_settings.json` in the extension's storage directory. Open it with the **MCP Servers** icon at the top of the Cline pane, then **Configure MCP Servers**.
| OS | Path |
| ------- | --------------------------------------------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |
| Windows | `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` |
| Linux | `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp",
"disabled": false,
"timeout": 900
}
}
}
```
`timeout` is in seconds here and it matters. Cline defaults to 60, and `verify_change` runs your whole test suite, so a real project blows past that and Cline reports a failure that is really a timeout. Cline stores this file separately from VS Code's own `.vscode/mcp.json`: configuring one does not configure the other.
## From a source checkout
Contributors running an unpublished build point the client at the compiled server with an absolute path:
```bash theme={null}
git clone https://github.com/Refactron-ai/refactron
cd refactron
npm install
npm run build
```
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "node",
"args": ["/absolute/path/to/refactron/dist/mcp/server.js"]
}
}
}
```
## Confirm it works before blaming the client
Send one `initialize` request straight to the binary:
```bash theme={null}
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
```
```json theme={null}
{
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": { "listChanged": true } },
"serverInfo": { "name": "refactron", "version": "0.3.0" }
},
"jsonrpc": "2.0",
"id": 1
}
```
If that works and your client still shows nothing, the problem is the client config: check the root key, the JSON or TOML validity, and whether the client inherits the `PATH` that holds `refactron-mcp`.
## Client requirements that trip people up
* **A test-suite run is the unit of work.** Clients with a default tool timeout under a minute will cut off a real verification. Raise the timeout where the client allows it, and check the unit: Cline and Codex CLI count seconds, Gemini CLI counts milliseconds.
* **Absolute paths only.** `repoRoot` must be an absolute path. Clients that pass a relative working directory will not help you here.
* **Tools need an agent mode.** Chat-only or ask-only modes in most editors do not call tools at all.
# MCP server
Source: https://docs.refactron.dev/mcp/overview
Refactron over the Model Context Protocol: one tool, verify_change, that returns SAFE, UNSAFE, or UNPROVEN on a proposed change before an agent lands it.
Refactron ships an [MCP](https://modelcontextprotocol.io) server that exposes exactly one tool: **`verify_change`**. An agent hands it a repository root and a proposed change, and gets back a verdict backed by your real test suite, run in an isolated shadow tree.
The server speaks **stdio**, identifies itself as `refactron` version `0.3.0`, and runs entirely on your machine. It never writes to your repository: applying the change stays the agent's job, after a verdict the agent trusts.
The fastest way in is to let your agent do the setup. Every client page below opens with a copy-only prompt that installs the server, registers it, reloads the client, and gets a first real verdict, written for that client specifically.
Prefer to do it by hand? Pick your client below, or follow the [MCP quickstart](/mcp/quickstart).
## Why an agent calls it
A coding agent has no independent way to tell a change that works from a change that only looks right. `verify_change` gives it one: run the project's own tests against the proposal, in a copy, and report what that run actually proved.
* **`SAFE`**: every gate passed and your tests exercised the changed code. Suite-approved, not proven correct: the verdict inherits exactly what your tests check.
* **`UNSAFE`**: a gate failed. The change broke something.
* **`UNPROVEN`**: the gates passed, but the evidence for `SAFE` is missing. A warning, not a rejection.
The full model, including how coverage fuses with the gates and what `SAFE` does not claim, is in [Verdicts](/verification/verdicts).
## Install
Requires Node.js 18 or newer. For a coverage-backed `SAFE` verdict you also need Python 3.8+ with `pytest` and `coverage.py`.
Install globally and get both binaries on your `PATH`:
```bash theme={null}
npm install -g refactron
```
That gives you `refactron` (the CLI) and `refactron-mcp` (this server).
To skip the install and let the client fetch the package on demand:
```bash theme={null}
npx -y -p refactron refactron-mcp
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own, so `npx refactron-mcp` on a machine without the global install looks for a package named `refactron-mcp` and fails with a registry 404.
`pip install refactron==0.3.0` installs a wrapper that still requires Node.js 18+, and it provides
the `refactron` command only.
The PyPI wrapper does not ship `refactron-mcp`. If you found Refactron through PyPI and want the MCP server, install the npm package as well.
## Connect your client
One `claude mcp add` command, or a project `.mcp.json`.
Edit `claude_desktop_config.json` and restart the app.
`~/.codex/config.toml`, in TOML, under `[mcp_servers.refactron]`.
Project `.cursor/mcp.json` or the global `~/.cursor/mcp.json`.
`.vscode/mcp.json`, whose root key is `servers`, not `mcpServers`.
One `gemini mcp add` command, or `~/.gemini/settings.json`.
Cascade reads `~/.codeium/windsurf/mcp_config.json`.
Anything that can launch a stdio MCP server can run this one, Zed and Cline included.
## Make it verify, not just connect
Registering the server only adds a tool the agent may never reach for. These two prompts are what turn it into a habit. Both are written for the agent, not for you.
### Verify before you apply
Defines the loop: propose, verify, then act on the verdict. Lasts the current conversation.
```text theme={null}
From now on, in this repository, verify every code change with the Refactron
verify_change MCP tool BEFORE you write it to disk.
The loop:
1. Work out the change you want to make. Do not apply it yet.
2. Call verify_change with:
repoRoot the ABSOLUTE path to this repository
edits [{ path, newContent }] with the full proposed contents of each
file you want to change, OR unifiedDiff, a unified diff of the
change against the CURRENT working tree
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified. Use PYTHONPATH=src for a
src layout. Write the command in module form (`python3 -m
pytest`), NOT as a bare console script (`pytest`): a console
script cannot be measured on Windows or under pyenv, asdf and
nix, and coverage then reports that it could not be determined.
3. Read the `verdict` field and act on it.
SAFE Apply the change. Report the verdict and quote the `reason`. Also
say what SAFE does not cover: your suite ran the changed code and
stayed green, which is only as strong as its assertions. If
coverage.changedStatements shows a low covered/total ratio, say so
rather than presenting SAFE as full coverage.
UNSAFE Do NOT apply anything. A gate failed, which means your change broke
something. Read `reason` and the failing entry in `gates`, fix YOUR
change, and call verify_change again. Repeat until it is not UNSAFE.
Never work around this by weakening or deleting a test. If you touch
a test file, the report lists it in testFilesChanged and I will check.
UNPROVEN Do NOT apply yet. Read `reason` to find out which kind this is:
"the changed code is not exercised by any test" means the suite ran
and nothing executed your lines. Write the tests named in the
`missingTests` array, add them to the same verify_change call, and
re-verify. Aim to turn this into SAFE. If a missingTests hint says a
line is excluded from coverage (`# pragma: no cover`, `if
TYPE_CHECKING:`), do NOT try to write a test for it. No test can
reach it. Flag it for human review.
"coverage of the changed code could not be determined" means no
measurement happened at all. Coverage is Python-only today, so a
TypeScript, JavaScript, or mixed-language change always lands here
and no test you write moves it. The gates still passed. Report this
plainly and let me decide. Do not present it as SAFE.
"no test runner detected" or "baseline tests already fail" means
nothing could be proven either way. Stop and tell me.
4. Never describe a SAFE verdict as "verified correct", "proven correct", or
"guaranteed safe". It means the project's own tests ran the changed code and
stayed green. It inherits exactly what those tests check.
5. If the tool returns an error instead of a verdict, read the message. "no
edits provided" means you passed neither `edits` nor `unifiedDiff`. "diff did
not apply (stale base?)" means you already wrote the change to disk, so the
diff no longer applies to the tree being copied: pass `edits` instead, or
revert first. A message about deletions, renames, copies, or binary files
means the change uses an operation the tool refuses to verify, so tell me and
verify that part by hand.
```
### Make it stick across sessions
Writes that rule into the project, so every session and every teammate inherits it.
```text theme={null}
Add a Refactron verification rule to this project's agent rules file, so the
behavior persists across sessions.
Pick the right file for this repo, and create it if it is missing:
- AGENTS.md at the repo root, the cross-tool convention
- CLAUDE.md at the repo root, for Claude Code
- .cursor/rules/refactron.mdc, or .cursorrules on older Cursor, for Cursor
- .windsurfrules for Windsurf
- .github/copilot-instructions.md for GitHub Copilot
- GEMINI.md at the repo root, for Gemini CLI
Append this section verbatim. Do not remove anything already in the file.
## Verification (required)
Verify every code change with the Refactron `verify_change` MCP tool before
writing it to disk. Pass `repoRoot` as an absolute path and the change as
`edits` or `unifiedDiff`. Verify before the change exists on disk: the shadow
tree copies the working tree, so a diff of an already-written change will not
apply.
- `SAFE`: apply it, and report the verdict. `SAFE` means the project's tests ran
the changed code and stayed green. It is suite-approved, not proven correct:
every changed statement a test could reach was executed, but a green suite is
only as strong as its assertions.
- `UNSAFE`: do not apply. Fix the change and re-verify. Never weaken a test to
turn a verdict green.
- `UNPROVEN`: do not apply yet. If the reason names uncovered code, write the
tests listed in `missingTests` and re-verify. If the reason is that coverage
could not be determined, say so and ask a human. Coverage is Python-only, so a
non-Python change cannot reach `SAFE` today.
```
### Why the wording in these prompts matters
Every prompt tells the agent, more than once, not to report `SAFE` as "correct". That is deliberate.
`SAFE` means the gates passed and your suite exercised the changed code. It does not mean the change is right. A real example from Refactron's own hardening runs: changing `<=` to `<` in Jinja2's `truncate` filter genuinely changes behavior at the boundary, all 911 tests still pass, the changed line is covered, and the verdict is `SAFE`. The verdict is correct about what it claims and is still not a proof of correctness.
An agent that reports `SAFE` as "verified correct" has laundered a test-suite result into a guarantee. That is the one failure mode this tool exists to prevent, so the prompts spend words on it.
## Then use it
Install, register, confirm the tool is listed, and get a first real verdict.
The standing rule that makes your agent verify instead of guess.
Every `verify_change` argument, the response shape, and the error shape.
What each verdict is allowed to claim, and what it is not.
## What the server does and does not do
* **It runs entirely local.** Verification happens on your machine, in a shadow tree. Nothing about your code leaves it, and the handler makes no network calls.
* **It never mutates your repository.** `verify_change` reads your tree and applies the change in an isolated copy. Landing the change is the agent's job, after a verdict it trusts.
* **It is deterministic.** The same repository state and the same change produce the same verdict every time.
* **It needs no login.** The `verify-diff` CLI is auth-gated, but the MCP handler calls the local engine directly.
## Limits worth knowing before you start
Coverage attestation runs through `coverage.py`, so it is Python-only today. A TypeScript, JavaScript, or mixed-language change passes through the same three gates but caps at `UNPROVEN` with `coverage.tool` set to `none`, because Refactron cannot measure whether the changed statements ran. It will not guess, and a guess here would be a false `SAFE`.
Coverage also cannot see code that only runs in a subprocess. A change exercised solely through `subprocess.run` or a `multiprocessing` worker reads as `UNPROVEN` unless you wire up subprocess coverage yourself.
Diffs that delete, rename, or copy a file, or that change a binary file, are refused rather than partially verified. See [Unsupported operations](/verification/verify-diff#unsupported-operations).
There is no hosted CI gate. Refactron runs where you run it: your machine, or your own CI job.
# MCP quickstart
Source: https://docs.refactron.dev/mcp/quickstart
Install the Refactron MCP server, register it with one client, confirm the tool is listed, and get a first real verdict on a real change.
Ten minutes, end to end: install the server, register it with one client, confirm the client sees `verify_change`, then verify a change and read the verdict.
## 1. Install
Requires Node.js 18 or newer.
```bash theme={null}
npm install -g refactron
```
Confirm both binaries landed:
```bash theme={null}
refactron --version
refactron-mcp --help 2>/dev/null || echo "refactron-mcp is on PATH"
```
`refactron-mcp` has no help output. It is an MCP server, not a CLI: it reads JSON-RPC on stdin and writes it on stdout, and it exits when stdin closes.
For a coverage-backed `SAFE` verdict, you also need Python 3.8+ with `pytest` and `coverage.py` in the environment that runs your tests:
```bash theme={null}
pip install pytest coverage
```
## 2. Smoke test the server without a client
Send one `initialize` request and read the identity back. This proves the binary runs before you blame a client config.
```bash theme={null}
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
```
```json theme={null}
{
"result": {
"protocolVersion": "2024-11-05",
"capabilities": { "tools": { "listChanged": true } },
"serverInfo": { "name": "refactron", "version": "0.3.0" }
},
"jsonrpc": "2.0",
"id": 1
}
```
`serverInfo.name` is `refactron` and `serverInfo.version` matches the package you installed. If this prints nothing, the problem is the install, not the client.
## 3. Register it with one client
Pick the client you actually use. Claude Code is the shortest path:
```bash theme={null}
claude mcp add refactron -- refactron-mcp
```
Every other client is a small JSON file. The canonical block, which Claude Desktop, Cursor, Windsurf, and Cline all accept:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
VS Code uses `servers` instead of `mcpServers`, and Codex CLI uses TOML rather than JSON. Per-client file paths and verification steps live on the client pages: [Claude Code](/mcp/claude-code), [Claude Desktop](/mcp/claude-desktop), [Codex CLI](/mcp/codex), [Cursor](/mcp/cursor), [GitHub Copilot in VS Code](/mcp/vscode), [Gemini CLI](/mcp/gemini-cli), [Windsurf](/mcp/windsurf), [anything else](/mcp/other-clients).
Prefer not to do this by hand? Each [client page](/mcp/overview#connect-your-client) opens with a copy-only prompt that has your agent install, register, reload, and verify for you.
## 4. Confirm the tool is listed
Whatever the client, you are looking for one tool named `verify_change` under a server named `refactron`. In Claude Code:
```bash theme={null}
claude mcp list
```
```text theme={null}
refactron: refactron-mcp - ✔ Connected
```
Inside a session, `/mcp` shows the same thing with the tool list expanded. If the server is listed but the tool is not, the client connected and the handshake failed; re-run the smoke test in step 2.
## 5. Get a first real verdict
Make a change in a Python project with a passing test suite, then ask your agent to verify it instead of applying it:
```text theme={null}
Use the refactron verify_change tool on this repository before you apply anything.
Pass repoRoot as the absolute path to this project and testCmd as "python3 -m pytest -q".
Show me the verdict and the reason, and do not write any files yet.
```
A real response on a covered change:
```json theme={null}
{
"verdict": "SAFE",
"reportVersion": 1,
"gates": {
"syntax": { "passed": true, "durationMs": 96 },
"imports": { "passed": true, "durationMs": 47 },
"tests": { "passed": true, "durationMs": 1370 }
},
"changedFiles": ["calc.py"],
"testFilesChanged": [],
"coverage": {
"tool": "coverage.py",
"changedLinesCovered": true,
"uncovered": [],
"filesWithUncovered": 0,
"changedStatements": { "total": 1, "covered": 1 },
"inertOnlyFiles": []
},
"reason": "Tests pass and the changed code is covered."
}
```
## What success looks like
* `claude mcp list` (or the equivalent panel in your client) shows `refactron` connected.
* The tool list contains exactly one entry: `verify_change`.
* A call returns JSON with a `verdict` field and a `reportVersion` of `1`.
* Your working tree is unchanged. Check with `git status`: verification happens in a shadow copy.
## If the verdict is not what you expected
An `UNPROVEN` on an all-Python change usually means one of three things, and the `reason` field tells you which.
| Reason text | What happened |
| ---------------------------------------------------------- | --------------------------------------------------------------------- |
| "the changed code is not exercised by any test" | The suite ran and passed, but nothing executed the lines you changed. |
| "coverage of the changed code could not be determined" | Coverage could not be measured at all. Different problem, see below. |
| "no test runner detected" or "baseline tests already fail" | Refactron cannot blame the change for anything, so it proves nothing. |
Those first two are different sentences on purpose. "Not exercised by any test" means the measurement happened and found nothing running your lines: write the test named in `missingTests` and re-verify. "Could not be determined" means no measurement happened at all: the diff is not all Python, or `coverage.py` is missing, or the tests never loaded the copy under verification.
That last case is the one that catches people. If your project is installed into the environment, an editable `pip install -e .` included, `import yourpackage` resolves to the installed copy and your tests exercise the original code rather than the change. Put the verified tree first on `sys.path` by passing it in the test command:
```text theme={null}
testCmd: "PYTHONPATH=. python3 -m pytest -q"
```
Use `PYTHONPATH=src` for a src layout. Relative paths resolve from the copy being verified, which is exactly what you want. Full detail in [Make sure the tests run the code being verified](/verification/verdicts#make-sure-the-tests-run-the-code-being-verified).
A mixed-language or non-Python diff caps at `UNPROVEN` no matter what you do. Coverage is Python-only today, and no test you add moves that verdict.
## Next
Blocks you can paste so your agent verifies before it applies, every time.
Arguments, request and response examples, and the error shape.
# verify_change reference
Source: https://docs.refactron.dev/mcp/tool-reference
The verify_change MCP tool contract: arguments, request and response examples, the error shape, and how it maps to verify-diff exit codes.
The Refactron MCP server exposes exactly one tool. Every example below was captured from a real stdio session against `refactron` 0.3.0.
## Arguments
| Argument | Type | Required | Description |
| ------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `repoRoot` | string | yes | Absolute path to the repository root. |
| `edits` | `[{ path, newContent }]` | see note | Proposed full-file contents. `path` is repo-relative. |
| `unifiedDiff` | string | see note | A unified (git) diff to apply and verify. |
| `testCmd` | string | no | Override the test command. Drives both the tests gate and the coverage run. A command that names a subset of the suite floors the verdict at `UNPROVEN`; see below. |
`repoRoot` is the only argument the schema marks required, but the handler needs a change to verify: supply **either** `edits` **or** `unifiedDiff`. Supplying neither returns an error result, not a verdict.
Do not narrow `testCmd` to the tests you just wrote. A command naming specific test paths, or
using `-k` / `-m` / `-t` / `--onlyChanged`, is classified as `narrowed` and will not return
`SAFE` — the best available verdict becomes `UNPROVEN`. Coverage cannot detect this on its own:
the changed code can be fully exercised by the subset you chose while the test that would have
failed was never selected. The result is reported on `testScope`. A `PYTHONPATH=` prefix is not
narrowing, and an exported `PYTEST_ADDOPTS` carrying a filter is caught too.
Detection covers the runners and flags Refactron knows; a command using an
unrecognised plugin flag is reported `unknown`, which does not cap the verdict.
Run the bare command if you need certainty.
## Request: the `edits` form
Use this when the agent already holds the full new contents of each file.
```json theme={null}
{
"method": "tools/call",
"params": {
"name": "verify_change",
"arguments": {
"repoRoot": "/Users/you/projects/demo",
"edits": [
{
"path": "calc.py",
"newContent": "def add(a, b):\n return b + a\n\n\ndef scale(a, factor):\n return a * factor\n"
}
],
"testCmd": "python3 -m pytest -q"
}
}
}
```
## Request: the `unifiedDiff` form
Use this when the change already exists as a patch, for example from `git diff` or a codemod.
```json theme={null}
{
"method": "tools/call",
"params": {
"name": "verify_change",
"arguments": {
"repoRoot": "/Users/you/projects/demo",
"unifiedDiff": "diff --git a/calc.py b/calc.py\n--- a/calc.py\n+++ b/calc.py\n@@ -3,4 +3,4 @@ def add(a, b):\n \n \n def scale(a, factor):\n- return a * factor\n+ return factor * a\n",
"testCmd": "python3 -m pytest -q"
}
}
}
```
## Response
The tool returns the same report `verify-diff --json` prints, serialized as text content. A real `UNPROVEN` response:
```json theme={null}
{
"verdict": "UNPROVEN",
"reportVersion": 1,
"engineVersion": "0.4.0",
"gates": {
"syntax": { "passed": true, "durationMs": 108 },
"imports": { "passed": true, "durationMs": 45 },
"tests": { "passed": true, "durationMs": 1423 }
},
"changedFiles": ["calc.py"],
"testFilesChanged": [],
"coverage": {
"tool": "coverage.py",
"changedLinesCovered": false,
"uncovered": [{ "file": "calc.py", "line": 6 }],
"filesWithUncovered": 1,
"changedStatements": { "total": 1, "covered": 0 },
"inertOnlyFiles": []
},
"reason": "Tests pass, but the changed code is not exercised by any test.",
"missingTests": [{ "file": "calc.py", "hint": "add a test exercising calc.py:6" }]
}
```
### Field by field
| Field | Meaning |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `verdict` | `SAFE`, `UNSAFE`, or `UNPROVEN`. See [Verdicts](/verification/verdicts). |
| `reportVersion` | Schema version, currently `1`. Which **shape** you are holding: read it before relying on any field below. |
| `engineVersion` | Version of the engine that produced this report. Which **rules** produced the verdict. Absent on reports written before 0.4.1. |
| `gates` | `syntax`, `imports`, `tests`, each with `passed` and `durationMs`. A failed gate also carries `blockingReason`. |
| `changedFiles` | Repo-relative paths the change touches. |
| `testFilesChanged` | Subset of `changedFiles` matching test conventions. A note, never a verdict input. |
| `coverage.tool` | `coverage.py` when coverage was measured, `none` when it could not be. |
| `coverage.changedLinesCovered` | `true`, `false`, or `"unknown"`. The three states are distinct and must not be collapsed. |
| `coverage.uncovered` | One entry per unexercised statement, at the statement's first line. Populated on `SAFE` too. |
| `coverage.changedStatements` | `{ total, covered }` across the diff, so you can read the ratio rather than only the boolean. Advisory: it does not feed the verdict. |
| `coverage.filesWithUncovered` | Distinct files with at least one uncovered statement, counted before any cap. |
| `coverage.unknownReason` | Why coverage is `"unknown"`, when Refactron knows. |
| `reason` | One sentence explaining the verdict. Machine-stable enough to branch on by substring. |
| `missingTests` | Concrete hints for `UNPROVEN` by coverage. Capped at 50. |
| `flakyTests` | Tests that failed once then passed on retry. Present only when it happened. Disqualifies `SAFE`. |
`coverage.removalOnlyFiles` and `coverage.inertOnlyFiles` list changed files with nothing for coverage to attest, because the change only removed lines or only touched comments and blank lines.
`coverage.uncoveredTruncated` and `missingTestsTruncated` appear as `{ shown, total }` whenever a list was capped, so a truncated report never reads as a complete one. An agent must check for them before concluding a file is absent from the list because it is fine.
### The two `UNPROVEN` reasons are not interchangeable
`"Tests pass, but the changed code is not exercised by any test."` means the measurement ran and found nothing executing your lines. Writing the test named in `missingTests` moves the verdict.
`"Tests pass, but coverage of the changed code could not be determined."` means no measurement happened. The diff was not all Python, `coverage.py` was unavailable, or the tests never loaded the copy under verification. No test you write moves this one.
Treating those as the same sentence is how a reader talks themselves into trusting a verdict that proved nothing. They are reported separately on purpose.
## Errors
Failures come back as a normal tool result with `isError` set, so the server keeps running and the agent can read the message.
```json theme={null}
{
"content": [
{
"type": "text",
"text": "verify_change failed: verifyDiff: no edits provided (pass `edits` or `unifiedDiff`)"
}
],
"isError": true
}
```
Real messages you will see:
| Message | Cause |
| ----------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `verifyDiff: no edits provided (pass \`edits\` or \`unifiedDiff\`)\` | Neither change argument was supplied. |
| `ENOENT: no such file or directory, scandir '/nope/does/not/exist'` | `repoRoot` does not exist, or is not absolute. |
| `diff deletes calc.py; file deletions are not supported yet, verify that change manually` | The diff deletes a file. |
| `diff renames to ; renames are not supported yet` | The diff renames a file. |
| `diff copies to ; copies are not supported yet` | The diff copies a file. |
| `diff contains only binary changes; nothing verifiable` | Binary-only diff. |
| `diff contains binary changes alongside text edits; binary changes cannot be verified` | Mixed binary and text diff. |
| `diff did not apply to calc.py (stale base?)` | The diff does not apply to the current tree. |
The stale-base error has one cause that surprises people. The shadow tree is a copy of your **working tree**, not of `HEAD`, so a change you have already written to disk is already in the copy. Passing `git diff` of that change asks Refactron to apply it a second time, and it will not apply. Verify before you write: pass the proposal as `edits`, or revert the file first and pass the diff.
Deletions, renames, copies, and binary changes are refused rather than partially verified. A diff that removed a module while making one innocuous edit once verified as safe on the half that could be checked, and applying it broke every import in the package. A partial verdict must never read as a verdict on the whole change.
## How it maps to the CLI
`verify_change` and `verify-diff` run the same engine and produce the same report. MCP has no exit codes, so the equivalence is with the `verdict` field:
| `verdict` | `verify-diff` exit code |
| ---------- | ----------------------- |
| `SAFE` | `0` |
| `UNPROVEN` | `0` |
| `UNSAFE` | `1` |
`UNPROVEN` exiting `0` is deliberate. It is a warning that nothing was proven, not a finding that something is broken, so it never silently blocks a merge. An agent that wants stricter behavior branches on the `verdict` field itself.
A tool result with `isError: true` corresponds to the CLI's exit `2`, bad input, which is an operational error rather than a verdict.
The `verify-diff` CLI is auth-gated and exits `7` when unauthenticated. The MCP handler calls the
local engine directly, so it needs no login and makes no network calls.
## Constraints
Coverage runs through `coverage.py` and is Python-only. A diff touching any non-Python file returns `UNPROVEN` with `coverage.tool` set to `none`, and cannot reach `SAFE` today. The gates still run.
Coverage cannot see subprocesses. Code exercised only inside `subprocess.run`, a `multiprocessing` worker, or a spawned server does not register as covered unless you wire up subprocess coverage yourself.
If the project is installed into the environment, an editable `pip install -e .` included, the tests may import the installed copy rather than the tree under verification, and the run proves nothing about the change. Prefix `testCmd` with `PYTHONPATH=.` (or `PYTHONPATH=src` for a src layout) so the verified copy wins on `sys.path`, and write the command in module form: `PYTHONPATH=. python3 -m pytest -q`. A bare console script such as `pytest` cannot always be measured, for the reason in [Make sure the tests run the code being verified](/verification/verdicts#make-sure-the-tests-run-the-code-being-verified).
# VS Code
Source: https://docs.refactron.dev/mcp/vscode
Add the Refactron MCP server to VS Code with GitHub Copilot. Note the root key: VS Code uses servers, not mcpServers.
VS Code reads MCP servers for GitHub Copilot's agent mode. Its config shape differs from every other client on one point, and it is the mistake people make most often.
## Set it up with your agent
One prompt, written for GitHub Copilot in VS Code to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in GitHub Copilot in VS Code.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Create `.vscode/mcp.json` in the repository root.
The root key here is "servers", NOT "mcpServers", and each server needs
"type": "stdio". This is the mistake people make most often: a block copied
from Claude Desktop or Cursor loads nothing here, silently, with no error.
{ "servers": { "refactron": { "type": "stdio",
"command": "refactron-mcp" } } }
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "servers": { "refactron": { "type": "stdio", "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"] } } }
If step 5 reports `spawn refactron-mcp ENOENT`, the binary is not on the PATH
VS Code inherited. Use the absolute path from `which refactron-mcp` as
"command".
STEP 4 - Reload.
Run "MCP: List Servers" from the Command Palette and start `refactron`.
If you cannot reach the Command Palette yourself, say so plainly and tell me
exactly what to click. Wait for me before you continue.
STEP 5 - Confirm the tool is exposed.
"MCP: List Servers" shows `refactron` with a Running status. Selecting it
offers Show Output and the start command.
In the Copilot Chat pane the mode selector must be on Agent, and then the tools
picker lists `verify_change` under `refactron`. Ask and Edit modes do not call
tools at all.
GitHub Copilot CLI keeps its own MCP config and expects "mcpServers".
Configuring VS Code does not configure the CLI.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## The root key is `servers`, not `mcpServers`
Claude Desktop, Cursor, Windsurf, and Cline all use `mcpServers`. VS Code uses `servers`. A config copied from any of those clients loads nothing here, with no error: VS Code simply sees no servers.
## Config location
| Scope | Path | Applies to |
| --------- | ----------------------------------------- | ------------------- |
| Workspace | `.vscode/mcp.json` in the repository root | This workspace only |
| User | `mcp.json` in your VS Code profile folder | Every workspace |
Open the user file from the Command Palette with **MCP: Open User Configuration**. For the workspace file, create `.vscode/mcp.json` yourself.
## Add it
```json theme={null}
{
"servers": {
"refactron": {
"type": "stdio",
"command": "refactron-mcp"
}
}
}
```
To pin the version and skip the global install:
```json theme={null}
{
"servers": {
"refactron": {
"type": "stdio",
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"]
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
## Verify the connection
Run **MCP: List Servers** from the Command Palette. `refactron` appears with a `Running` status; select it for **Show Output** and the start command.
In the Copilot Chat pane, switch the mode selector to **Agent**, then open the tools picker. `verify_change` is listed under `refactron` with a checkbox.
## Troubleshooting
* **No servers listed**: check the root key. It is `servers`. A copied `mcpServers` block is the usual cause.
* **The server sits at `Stopped`**: open **MCP: List Servers**, select `refactron`, then **Start Server**, and read **Show Output** for the launch error.
* **`spawn refactron-mcp ENOENT`**: the binary is not on the `PATH` VS Code inherited. Run `which refactron-mcp` in a terminal and use that absolute path as `command`, or use the `npx` form.
* **The tool never gets called**: Copilot only calls tools in **Agent** mode. Ask and Edit modes do not.
* **GitHub Copilot CLI does not read this file**: the CLI keeps its own MCP config and expects `mcpServers`. Configuring VS Code does not configure the CLI.
* **A verification takes minutes**: `verify_change` runs your real test suite in a shadow copy.
# Windsurf
Source: https://docs.refactron.dev/mcp/windsurf
Add the Refactron MCP server to Windsurf Cascade by editing ~/.codeium/windsurf/mcp_config.json.
Windsurf keeps MCP servers in a single file under your home directory, shared across every workspace. Cascade picks them up after a refresh.
## Set it up with your agent
One prompt, written for Windsurf to read rather than you. It installs the
server, registers it, reloads, and proves the tool works with a real
verification.
```text theme={null}
Set up the Refactron MCP server for yourself in this repository and prove it
works before you tell me it is done. Do the steps in order.
You are running in Windsurf.
STEP 1 - Check the prerequisites.
Run `node --version`. Refactron needs Node.js 18 or newer. If it is older or
missing, stop and tell me.
Run `python3 --version`, then check that `pytest` and `coverage` are
importable. Coverage attestation is Python-only, and it is what makes a
coverage-backed SAFE verdict possible. Without it the server still runs and
still gates the change, but every verdict caps at UNPROVEN.
STEP 2 - Install the server.
Run `which refactron-mcp`, or `where refactron-mcp` on Windows. If it prints a
path, go to step 3. Otherwise install it:
npm install -g refactron
That puts two binaries on PATH: `refactron` (the CLI) and `refactron-mcp` (the
MCP server). If you cannot install globally, install nothing and use the npx
form in step 3 instead.
Do not use `pip install refactron` for this. The PyPI wrapper provides the
`refactron` command only. It does not ship `refactron-mcp`.
STEP 3 - Register the server.
Edit `~/.codeium/windsurf/mcp_config.json`, or
%USERPROFILE%\.codeium\windsurf\mcp_config.json on Windows, root key
"mcpServers". This one file is shared across every workspace.
{ "mcpServers": { "refactron": { "command": "refactron-mcp" } } }
Merge into the existing object if the file already has servers, and validate
the JSON. Windsurf drops the whole file when it cannot parse it, so one stray
comma removes every server I have.
Without a global install, use this instead. The `-p` flag is required, because
`refactron-mcp` is a second binary of the `refactron` package and not a package
of its own:
{ "mcpServers": { "refactron": { "command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"] } } }
STEP 4 - Reload.
Windsurf reads the config on refresh or restart, never on save.
You cannot do this yourself. Tell me to click Refresh in the Cascade MCP panel,
under the hammer icon, and wait for me before you continue.
STEP 5 - Confirm the tool is exposed.
Ask me to confirm that `refactron` is listed with a green dot under the hammer
icon in the Cascade panel, with one tool beneath it.
Windsurf caps how many tools it exposes to Cascade. Refactron adds exactly one,
but if I am near the cap, tell me which server to disable.
You are looking for a server named `refactron` exposing exactly
ONE tool: `verify_change`.
If the server is connected but no tool appears, the handshake failed. Prove the
binary itself works by sending it one initialize request:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1.0"}}}' | refactron-mcp
A working server replies on one line with serverInfo.name "refactron". If that
works and the client still shows nothing, the fault is the config: check the
root key and check that the file parses.
STEP 6 - Prove it works with one real verification.
Do not skip this. A connected server is not a working server.
Pick one small source file that this project's test suite exercises. Propose a
real but behaviour-preserving edit to it: reorder the operands of a commutative
expression, rename a local variable, that kind of thing. One or two lines.
Do NOT write that edit to disk. Call verify_change with the proposal instead:
repoRoot the ABSOLUTE path to this repository
edits [{ "path": "",
"newContent": "" }]
testCmd the project's test command, if it is not obvious. For a Python
project installed into the environment (an editable
`pip install -e .` included), prefix it with PYTHONPATH=. so the
tests import the copy being verified rather than the installed
one. Use PYTHONPATH=src for a src layout.
Write the command in module form (`python3 -m pytest`), NOT as a
bare console script (`pytest`): a console script cannot be measured
on Windows or under pyenv, asdf and nix, and coverage then reports
that it could not be determined.
Verify the change BEFORE it exists on disk. The shadow tree is a copy of the
working tree, so if you write the change first and then pass a `git diff` of
it, the diff no longer applies and you get "diff did not apply (stale base?)"
back instead of a verdict.
Verification runs the whole test suite in a shadow copy, so it can take minutes
on a real project. That is the work, not a hang.
STEP 7 - Report, then stop.
Tell me:
- which file you wrote, or which command you ran
- the exact `verdict` and the exact `reason` from the response
- the value of `coverage.tool`: "coverage.py" means coverage was measured,
"none" means it was not
Apply nothing. The step 6 edit was a probe. Confirm with `git status` that the
working tree is exactly as dirty as you found it, and no more.
Do not describe a SAFE verdict as "correct", "proven", or "guaranteed". SAFE
means the project's own tests ran the changed code and stayed green. It
inherits exactly what those tests check, and a weak suite yields a weak SAFE.
```
Prefer to do it yourself? The rest of this page is the same setup by hand.
## Config location
| OS | Path |
| ------------ | ------------------------------------------------- |
| macOS, Linux | `~/.codeium/windsurf/mcp_config.json` |
| Windows | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` |
Windsurf can open it for you: in the Cascade panel, click the hammer icon, then **Configure**.
## Add it
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
To pin the version and skip the global install:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "npx",
"args": ["-y", "-p", "refactron", "refactron-mcp"]
}
}
}
```
The `-p` flag is required. `refactron-mcp` is a second binary of the `refactron` package, not a package of its own.
Save the file, then click **Refresh** in the Cascade MCP panel. Windsurf does not reload the file on save.
## Verify the connection
Click the hammer icon in the Cascade panel. `refactron` is listed with a green dot and one tool: `verify_change`.
Ask Cascade to confirm:
```text theme={null}
List the tools you have from the refactron MCP server.
```
## Troubleshooting
* **Nothing changes after editing the file**: press **Refresh** in the MCP panel. Windsurf reads the config on refresh or restart, never on save.
* **The server is listed but red**: the command did not launch. Run `which refactron-mcp`. If it prints nothing, run `npm install -g refactron`, or use the `npx` form above.
* **Invalid JSON**: Windsurf drops the whole file when it cannot parse it, so one stray comma removes every server. Validate before refreshing.
* **Tool call limits**: Windsurf caps the number of tools it exposes to Cascade. Refactron adds exactly one, but if you are near the cap, disable a server you are not using.
* **A verification takes minutes**: `verify_change` runs your real test suite in a shadow copy. That is the work, not a hang.
# Overview
Source: https://docs.refactron.dev/overview
Prove that any change, your AI agent's, a codemod's, or your own, preserves the behavior your tests check, or find out it can't be proven. Refactron runs your real tests in an isolated shadow tree and returns a reproducible SAFE, UNSAFE, or UNPROVEN verdict.
## What is Refactron?
Refactron is the **verification layer for AI code change**. As agents write more of your code, the bottleneck moves from *generating* changes to *trusting* them. Refactron is the deterministic gate that answers one question about any diff (your AI agent's, a codemod's, or your own):
> **Did this change preserve the behavior your tests check, and can you prove it?**
It applies the change in an isolated **shadow tree**, runs three gates against it (syntax, then imports, then your real test suite) and checks whether your tests actually exercise the lines that changed. It returns one of three verdicts. Your working tree is never touched.
## The three-way verdict
Every gate passed **and** your tests exercised every changed statement they could reach. Proven
safe.
A gate failed: the change broke something. Exit `1`.
Tests pass, but the changed code isn't exercised, so it isn't *proven*.
`UNPROVEN` is the verdict that makes the other two trustworthy. "Tests pass" is not the same as "this change is proven safe": if nothing runs the lines you changed, a green suite proves nothing about them. Most tools hand-wave this. Refactron says it out loud and tells you which test to add. See [Verdicts](/verification/verdicts) for the full model.
## Verify your first change
Install the CLI and `login` first (both are in the [quickstart](/quickstart); an unauthenticated run exits `7`):
```bash theme={null}
npm install -g refactron
refactron verify-diff . --diff change.diff --test-cmd "python3 -m pytest -q"
```
```text theme={null}
[UNPROVEN] Tests pass, but the changed code is not exercised by any test.
uncovered: calc.py:14
```
## Where it plugs in
Verify a diff from the command line or as a CI merge gate. `UNSAFE` fails the build.
A `verify_change` tool your AI agent calls before it lands a change.
SAFE, UNSAFE, UNPROVEN: coverage fusion and the Python-only limitation, in depth.
Coverage is **Python-only** (via `coverage.py`). A TypeScript or mixed-language diff can never
earn `SAFE` today; it returns `UNPROVEN` ("coverage of the changed code could not be determined").
The gates still run; only the coverage half is Python-only.
## Deterministic, on purpose
The verification engine is 100% deterministic: no model, no temperature, no statistical inference decides whether your change is safe. Read the same repo state and the same diff twice, get the same verdict. That determinism is exactly what makes Refactron the gate an LLM agent can *call*: the agent proposes, Refactron proves. See [Deterministic by design](/concepts/why-no-llm).
## Next steps
Install from npm and verify your first diff end to end.
The shadow tree, the three gates, and the atomic-write contract.
# Quickstart
Source: https://docs.refactron.dev/quickstart
Install Refactron from npm and verify your first diff end to end, then give the same gate to your AI agent over MCP.
In the next ten minutes you'll install Refactron, verify a real change against a real test suite, and read your first `SAFE` / `UNSAFE` / `UNPROVEN` verdict.
## Install
Requires Node.js ≥ 18.
```bash theme={null}
npm install -g refactron
```
That puts two binaries on your `PATH`: `refactron` (the CLI) and `refactron-mcp` (the MCP server). To skip the install, run `npx refactron ` instead.
To earn a `SAFE` verdict end to end you also need a Python project with a passing `pytest` suite, plus `coverage.py` for the coverage measurement:
```bash theme={null}
pip install pytest coverage
```
Prefer PyPI? `pip install refactron==0.3.0` installs a thin `refactron` shim that shells out to
the npm CLI. It is not a Node-free path: you still need Node.js ≥ 18 **and** `npm install -g
refactron`. When the npm CLI is missing, the shim prints the exact matching install command
and exits non-zero. The shim provides the `refactron` command only; `refactron-mcp` comes from the
npm package.
## Authenticate
`verify-diff` is auth-gated. Log in once on this machine (this runs a device-code flow and opens your browser to approve a code):
```bash theme={null}
refactron login
```
For CI or a headless box, export a token instead of the browser flow:
```bash theme={null}
export REFACTRON_TOKEN=refactron_live_...
```
An unauthenticated run exits `7`.
## Verify a diff
Point Refactron at a repo and a unified diff. The diff can come from anywhere: an agent, a codemod, or your own `git diff`.
From a Python project with a test suite:
```bash theme={null}
cd /path/to/your/project
git diff > change.diff # or: your agent wrote change.diff
# already staged or committed? use `git diff --staged` or `git diff main...HEAD`
```
Run `verify-diff` from inside the project you just captured the diff from:
```bash theme={null}
refactron verify-diff . --diff change.diff --test-cmd "python3 -m pytest -q"
```
Refactron copies your project into an isolated shadow tree, applies the diff there,
runs the gates, and measures whether your tests exercise the changed lines. Your
real working tree is never modified.
Coverage is **Python-only**, so only an all-Python diff can return `SAFE`; a TypeScript or
mixed diff passes the gates and returns `UNPROVEN`. Form that expectation before the verdict
prints below.
One of three outcomes prints:
```text theme={null}
[SAFE] Tests pass and the changed code is covered.
```
```text theme={null}
[UNPROVEN] Tests pass, but the changed code is not exercised by any test.
uncovered: calc.py:14
```
```text theme={null}
[UNSAFE]
```
`SAFE` and `UNPROVEN` exit `0`; `UNSAFE` exits `1`, so it fails a CI job on its own.
What just happened:
* **`SAFE`**: the gates passed *and* your tests ran every changed statement they could reach. Proven.
* **`UNPROVEN`**: the gates passed, but the change isn't proven. Two causes, two responses:
* A line is named in the output (`uncovered: calc.py:14`): add a test that exercises it, then re-run to earn `SAFE`.
* The reason is "coverage of the changed code could not be determined": that's the [Python-only limit](/verification/verdicts#the-python-only-limitation) (a non-Python or mixed diff, or no `coverage.py`). The gates still passed, but no added test changes the verdict today.
* **`UNSAFE`**: a gate rejected the change. A tests-gate failure prints the failing-test tail; a syntax or imports failure prints that gate's reason.
Add `--json` for the full reproducible report. See [Verdicts](/verification/verdicts) for the complete model.
Coverage is **Python-only** (via `coverage.py`). A TypeScript or mixed-language diff can never
reach `SAFE` today; it returns `UNPROVEN` ("coverage of the changed code could not be
determined"). The gates still run; only the coverage half is Python-only.
## Give the gate to your agent (MCP)
The same engine is available as an [MCP](https://modelcontextprotocol.io) tool, `verify_change`, so an AI agent can verify a change before it lands it. Register the stdio server with your client. For Claude Code:
```bash theme={null}
claude mcp add refactron -- refactron-mcp
```
The agent proposes an edit (as full-file contents or a unified diff), calls `verify_change`, and gets back the same `SAFE` / `UNSAFE` / `UNPROVEN` report, then decides whether to land it. Full setup, for every client, in the [MCP tab](/mcp/overview).
## Build from source (contributors)
Working on Refactron itself? Build the checkout instead of installing the published package:
```bash theme={null}
git clone https://github.com/Refactron-ai/refactron
cd refactron
npm install
npm run build
```
The CLI is then `node dist/cli/index.js ` and the MCP server is `node dist/mcp/server.js`. Every command on this page works the same way; substitute that invocation for `refactron`.
## Next steps
Every `verify-diff` flag, exit code, and the JSON report shape.
The shadow tree, the three gates, and the atomic-write contract.
# MCP server
Source: https://docs.refactron.dev/verification/mcp-server
The Refactron MCP server has its own tab: setup for every major AI coding client, plus the verify_change tool contract.
Refactron ships an [MCP](https://modelcontextprotocol.io) server exposing one tool, **`verify_change`**, so an AI agent can verify a change before it lands. Setup and reference moved to the [MCP tab](/mcp/overview), which covers every major client.
What the server is, how to install it, and which client pages exist.
Install, register, confirm the tool is listed, and get a first verdict.
Arguments, request and response examples, and the error shape.
One prompt sets this up end to end. Another keeps it verifying before it applies.
## Connect your client
[Claude Code](/mcp/claude-code), [Claude Desktop](/mcp/claude-desktop), [Codex CLI](/mcp/codex), [Cursor](/mcp/cursor), [GitHub Copilot in VS Code](/mcp/vscode), [Gemini CLI](/mcp/gemini-cli), [Windsurf](/mcp/windsurf), or [any other MCP client](/mcp/other-clients), which covers Zed and Cline.
## The short version
Install the npm package, which puts `refactron-mcp` on your `PATH`:
```bash theme={null}
npm install -g refactron
```
Register it as a stdio server. For Claude Code:
```bash theme={null}
claude mcp add refactron -- refactron-mcp
```
For clients configured with JSON:
```json theme={null}
{
"mcpServers": {
"refactron": {
"command": "refactron-mcp"
}
}
}
```
VS Code uses the root key `servers` instead of `mcpServers`, and Zed uses `context_servers`. Per-client paths and verification steps are on the client pages linked above.
The tool returns the same report `verify-diff --json` prints. See [Verdicts](/verification/verdicts) for what each verdict is allowed to claim, and [Verify a diff](/verification/verify-diff) for the same engine from the command line.
# Verdicts
Source: https://docs.refactron.dev/verification/verdicts
SAFE, UNSAFE, and UNPROVEN: the three-way verdict at the center of Refactron, how coverage fuses with the gates, and why UNPROVEN is the honest answer.
Every verification returns exactly one of three verdicts. Two of them exist in most tools. The third, `UNPROVEN`, is the one that makes Refactron trustworthy.
Gates pass **and** your tests exercise the changed statements.
A gate failed. The change
broke something.
Tests pass, but the changed code isn't proven safe.
## How a verdict is decided
Refactron runs two independent checks and fuses them:
1. **The gates**: syntax, then imports, then your test suite, all against the change applied in an isolated [shadow tree](/concepts/safety-model). A gate either passes or fails.
2. **Changed-statement coverage**: did your tests actually execute the statements the change touched? Each changed line is mapped to the statement containing it first. Assessed only when the gates pass.
The fusion rule:
| Gates | Changed statements covered? | Verdict | Reason |
| ------------- | --------------------------- | ---------- | ----------------------------------------------------------------------- |
| A gate failed | n/a | `UNSAFE` | The failing gate's blocking reason. |
| All passed | yes | `SAFE` | "Tests pass and the changed code is covered." |
| All passed | no | `UNPROVEN` | "Tests pass, but the changed code is not exercised by any test." |
| All passed | couldn't tell | `UNPROVEN` | "Tests pass, but coverage of the changed code could not be determined." |
## SAFE
Every gate passed, and every changed **statement** your suite could reach was executed by it. The change is on a tested path, and that path is green.
`SAFE` is a per-**statement** check. Every changed statement your suite could
have executed did execute. A file with some exercised and some unexercised
changed statements reads as `UNPROVEN`, and the reason names the ratio:
```
[UNPROVEN] Tests pass, but only 3 of 14 changed statements were exercised.
```
Statements coverage.py **excluded** are subtracted from the count rather than
held against you. `# pragma: no cover` bodies and `if TYPE_CHECKING:` blocks can
never be executed by any test, so requiring them would put `SAFE` out of reach
for any change that adds a typing-only import. They are still listed in
`coverage.uncovered`, tagged `excluded`, with a hint telling you to review them
by hand instead of asking for an impossible test.
A change consisting *entirely* of excluded statements does not reach `SAFE`:
there is nothing a test could have proven about it.
`SAFE` also requires that changed **conditionals** were fully branched. A changed
`if`/`elif` whose header ran but one of whose branches no test entered floors at
`UNPROVEN`, even when statement coverage is complete: the behaviour that changed
sits in a branch nobody tested. The reason names the line so you know which test
to add:
```
[UNPROVEN] Tests pass, but a changed conditional has a branch no test took
(calc.py:3). Add a test that enters the other branch.
```
A conditional whose branches are all exercised still reaches `SAFE`, so this
never blocks a fully-tested change. Branch coverage is Python-only, like the rest
of the coverage check.
This covers statement-level branches — `if`, `elif`, `while`, `for`/`else`,
`match`. It does not cover an untested path inside a comprehension filter, a
ternary, or a boolean short-circuit (`a and b`): `coverage.py` does not report
those as branches, so a changed expression of that kind can still read `SAFE`
with a sub-path untested.
## `--mutate`: was the change actually asserted?
Coverage proves a changed statement *ran*. It does not prove any test would
*fail* if that statement's behaviour changed. A line a test executes but never
asserts on counts as covered and earns `SAFE`.
`refactron verify-diff --mutate` closes that gap. It perturbs the operators and
constants in your changed statements — a boundary (`<=` → `<`), an arithmetic
sign, a boolean `and`/`or`, or a constant (a number, a string, `True`/`False`/
`None`) — reruns your suite against each, and if any mutant **survives** (the
suite still passes), the verdict floors at `UNPROVEN`, naming the survivor:
```
[UNPROVEN] Tests pass, but a mutant of a changed statement survived
(calc.py:2, + to -): no test failed when its behaviour changed. Add a test
that asserts on it.
```
It is opt-in and slower — it reruns your suite once per mutant — so it is a
deep check, not the default. It is also **downgrade-only**: a surviving mutant
can move `SAFE` to `UNPROVEN`, but a clean mutation run never lifts a verdict.
An inconclusive mutant (one that times out) is skipped, not held against you.
Python-only, and bounded to the statements the diff changed.
This is stricter than it was in 0.4.0. The old rule cleared a whole file as
soon as **one** changed statement in it ran, so a diff changing 40 statements
with 1 exercised returned `SAFE`. If you are comparing against stored reports,
a `SAFE` whose `coverage.changedStatements` shows `covered < total` was earned
under the old rule.
## `--flaky-check`: was the green stable, or lucky?
A default `SAFE` assumes your tests are **deterministic**. The suite runs once; a
test that passes because of randomness, ordering, timing, or hash-seed dependence
— not because behaviour is preserved — still counts toward `SAFE`. The single-run
fast path cannot tell a stable green from a lucky one.
`refactron verify-diff --flaky-check` closes that gap. After a would-be-`SAFE`
verdict, it reruns your suite several times on fresh trees, each under a different
`PYTHONHASHSEED`, and if any test's outcome **varies** across the reruns, the
green was never stable, so the verdict floors at `UNPROVEN`, naming the flaky
test:
```
[UNPROVEN] Tests pass, but a test outcome varied across reruns
(tests/test_x.py::test_scale); the green is flaky, not stable. Fix the
flakiness or the verdict cannot be SAFE.
```
Like `--mutate` it is opt-in and slower — it reruns your whole suite K times
(default 3) — so it is a deep check, not the default. It is also
**downgrade-only**: a varied test can move `SAFE` to `UNPROVEN`, but a run where
every rerun agrees never lifts a verdict. A rerun that times out is inconclusive
(skipped), not held against you. Varying `PYTHONHASHSEED` catches dict/set
order-dependence deterministically; timing, network, and `random`-based flakes are
probabilistic, so K reruns give K chances rather than a guarantee.
Because the default gate runs the suite once, a **default** `SAFE` cannot detect a lucky-pass
flake. It is an honest verdict on the run it observed. If your suite has any non-determinism, use
`--flaky-check` before trusting a `SAFE`.
`SAFE` also requires that the suite was not narrowed. If your `testCmd` names a
subset of the suite, the verdict floors at `UNPROVEN` however well the changed
code is covered, because a green run of the tests you selected says nothing
about the tests you did not. See [A narrowed test command cannot be
SAFE](#a-narrowed-test-command-cannot-be-safe) below.
`SAFE` reports still list what they did not prove. `coverage.uncovered` is always populated, and `coverage.changedStatements` gives the ratio outright:
```json theme={null}
"coverage": {
"changedLinesCovered": true,
"changedStatements": { "total": 40, "covered": 39 },
"uncovered": [{ "file": "calc.py", "line": 31, "excluded": true }]
}
```
Thirty-nine of forty changed statements ran. The fortieth sits in a `# pragma: no cover` or `if TYPE_CHECKING:` block that no test can reach, so it is subtracted from what `SAFE` requires and still listed rather than hidden. That is the only shape of gap a `SAFE` can now carry: a verdict that concealed it would be easier to read and worth less.
A report showing `changedLinesCovered: true` with a genuinely unexercised statement (`"total": 40, "covered": 12` and no `excluded` flag) was produced by Refactron 0.4.0 or earlier, under the older per-file rule noted above.
`SAFE` requires coverage, and [coverage is Python-only](#the-python-only-limitation). A change that isn't entirely Python can't reach `SAFE` today.
## UNSAFE
A gate rejected the change:
* **Syntax**: the changed file no longer parses.
* **Imports**: an import in the changed content doesn't resolve, or a previously-resolving import now fails.
* **Tests**: your suite went red on the change.
`UNSAFE` exits `1`, and a failing test gate prints the test output so you can see what broke. This is a real signal that the change is wrong, not merely `UNPROVEN`.
One case looks like a test failure but isn't a real one: if your suite is **already red before**
the change, or **no test runner** is detected, Refactron can't blame the change for anything, so
it returns `UNPROVEN`, not `UNSAFE`. A broken baseline is a "can't prove it" situation, not
evidence the diff broke something.
## UNPROVEN
The gates passed, but Refactron won't claim the change is safe, because the evidence isn't there. This is the verdict no other gate gives you honestly.
There are two ways to land here:
* **The changed code isn't exercised.** Your tests pass, but none of them run the code you changed. A green suite tells you nothing about untested lines. Refactron lists each uncovered statement and, in the JSON report, a `missingTests` hint:
```json theme={null}
"missingTests": [
{ "file": "calc.py", "hint": "add a test exercising calc.py:14" }
]
```
* **Coverage couldn't be assessed.** The change isn't entirely Python, or `coverage.py` isn't installed. Refactron can't measure whether the changed statements ran, so it declines to certify `SAFE`. The reason reads "coverage of the changed code could not be determined."
`UNPROVEN` exits `0`. It is a warning, not a rejection: nothing is known to be broken, but nothing is proven either. The right response is to add the missing test, then re-run and earn `SAFE`.
`UNPROVEN` turns your test suite's blind spots into a to-do list. Every uncovered statement is a
test you could write to move the verdict to `SAFE`.
### Coverage is judged per statement, not per line
`coverage.py` records execution against the **first line of a statement**. A statement wrapped
across several lines, which is what any formatter produces, has continuation lines, closing
brackets, and trailing commas that `coverage.py` never marks at all.
Refactron therefore maps each changed line to the statement that **contains** it, using the Python
AST, before judging it. A changed continuation line counts as exercised when the statement it
belongs to ran, and one unexercised multi-line statement produces **one** entry, at the line you
would actually write a test against, not one entry per physical line. Without this, a reformat that
only rewraps code reports every wrapped line as uncovered: a `black` run over 28 files once produced
3666 such entries, almost all of them for code that provably executed.
Containment is the load-bearing word. The cheaper version of this idea, "walk back to the nearest
statement start at or above the changed line," is wrong in a way that manufactures false `SAFE`
verdicts: it cannot tell a continuation line of that statement from a blank line, a comment, or a
dead-branch line that merely *follows* it and belongs to somewhere else entirely. Under that rule an
executed `def` vouches for a body that never ran. Real extents from the AST answer the question
exactly.
### Blank lines and comments prove nothing, and are asked to prove nothing
A changed line carrying no code at all, a blank line or a comment-only line, is **inert**. It cannot
change behavior, so Refactron never reports it as uncovered; and it cannot be exercised by a test,
so it never counts toward a file's coverage either. Formatters move blank lines constantly, and a
mechanism that let them vouch for their neighbours would turn every reformat into a free `SAFE`.
A file whose changed lines are *all* inert has nothing to attest, and gets its own reason rather
than a pass:
```
[UNPROVEN] Tests pass. The change only touches comments and blank lines; there are no added
statements for coverage to attest.
```
That is deliberately conservative. A diff exposes only the lines it **adds**, so "every added line
is inert" is not the same claim as "this file is unchanged": a deleted statement next to a moved
blank line looks identical from here. Removal-only files are treated the same way and for the same
reason.
A docstring is a real statement in Python, so a docstring-only edit lands on its own statement and
is judged like any other. `coverage.py` does not track function docstrings, so such a change
typically reads as unexercised, which is the honest answer rather than a convenient one.
### Code your suite is not allowed to reach
Some statements can never be exercised, by design. A `# pragma: no cover` block, or an import under
`if TYPE_CHECKING:`, is excluded from `coverage.py`'s judgement and never executes under test. A
diff that touches only such code therefore **cannot reach `SAFE`**, no matter how good your suite
is: there is no execution to observe, and Refactron will not certify what it did not see.
These entries are marked, and their hints say what is actually true instead of asking for a test
that cannot exist:
```json theme={null}
"coverage": {
"uncovered": [{ "file": "gated.py", "line": 6, "excluded": true }]
},
"missingTests": [
{
"file": "gated.py",
"hint": "gated.py:6 is excluded from coverage (e.g. `# pragma: no cover`, `if TYPE_CHECKING:`), so no test can exercise it; review this change by hand"
}
]
```
The right response is a human review of that hunk, not a new test.
Large diffs are capped so the report stays readable. When Refactron truncates, it says so rather
than shipping a short list that looks complete:
```json theme={null}
"coverage": {
"uncoveredTruncated": { "shown": 200, "total": 412 },
"filesWithUncovered": 37
},
"missingTestsTruncated": { "shown": 50, "total": 412 }
```
Each file is guaranteed a share of the list before any file takes a second helping, so one
pathological file cannot consume every slot and push later files out of the report entirely.
`filesWithUncovered` counts distinct files **before** the cap, so you can always tell whether the
list you are reading spans the whole diff.
## The Python-only limitation
Coverage fusion depends on `coverage.py`, so it is **Python-only** today:
* A diff where **every** changed file is `.py`, with `coverage.py` available, can be assessed, and can reach `SAFE`.
* A diff touching **any** TypeScript (or any non-Python) file, or run without `coverage.py`, returns `UNPROVEN` with `coverage.tool: "none"`. The gates still run; only the coverage half is unavailable.
Refactron never guesses here. Reporting a non-Python change as "covered" would let an unverified change through as `SAFE`: a false `SAFE`, which the engine forbids. When it can't measure, it says `UNPROVEN`.
## Coverage cannot see subprocesses
Coverage fusion measures the lines your test process executes. Code that runs only in a child process, launched through `subprocess.run`, a `multiprocessing` worker, or a spawned server, is invisible to `coverage.py` unless you wire up subprocess coverage yourself: a `COVERAGE_PROCESS_START` environment variable plus a `coverage.process_startup()` call in `sitecustomize`.
Without that wiring, a change whose only exercise happens inside a subprocess reads as `UNPROVEN`. The gates still pass, but the changed statements never register as covered, so Refactron declines to certify `SAFE`. This is the honest result, not a defect. First-class subprocess coverage is a planned fast-follow; until it lands, exercise the changed code in-process in at least one test so the verdict can reach `SAFE`.
## SAFE means suite-approved, not proven correct
`SAFE` says your suite ran the changed code and stayed green. It does not claim the change is correct in some absolute sense: it inherits exactly what your suite checks. That includes side effects your suite never observes, an extra or missing database write, an email that stopped sending, a log a downstream system parses: Refactron sees a change only through your tests, so behavior no test watches is outside the verdict. Audits of AI agent patches (for example on SWE-bench) find that a substantial share of patches which pass the project's tests are still wrong, because the suite was too weak to catch the defect.
Refactron cannot turn a weak suite into a strong one, but it refuses to overstate what a green run proves. That is why `UNPROVEN` exists, and why every uncovered statement ships with a `missingTests` hint: the path to a `SAFE` you can trust is a suite that actually exercises the behavior you care about.
A worked example from our own hardening runs, on a real library. Jinja2's `truncate` filter guards its early return with `if len(s) <= length + leeway`. Change that `<=` to `<` and the behavior genuinely changes: at the exact boundary the string is now truncated instead of returned whole. All 911 tests in Jinja2's suite still pass, because none of them lands on that precise boundary, and the changed line is covered, so the verdict is `SAFE`.
That verdict is correct about what it claims (the suite ran this line and stayed green) and it is still not a proof of correctness. Boundary conditions are exactly where suites tend to be thin. Read `SAFE` as "your tests approve this change", then decide separately whether your tests are strong where this change lives.
## A narrowed test command cannot be SAFE
If you pass a `testCmd` that names a subset of the suite, the verdict floors at
`UNPROVEN`. Coverage can report the changed code as fully exercised while the
one test that would have caught the change was never selected.
**What this check can and cannot see.** Refactron reads the command string, the environment, and
your pytest configuration. It recognises `pytest`, `unittest`, `vitest` and `jest` and their
common flags — but not every flag of every plugin. A command carrying an option it does not
recognise is reported `unknown` rather than `full`, and `unknown` does not floor the verdict.
Treat this as a strong check on the shapes it knows, not a guarantee that no narrowing can ever
reach `SAFE`. If you need certainty, run the bare command.
A worked example. One file, one change (`return x * 2` to `return x * 3`), and
two tests: `test_scale` executes the changed line without pinning its value,
`test_report` pins it and fails. Only the command differs:
```bash theme={null}
# runs both tests; test_report catches the change
python3 -m pytest -q # UNSAFE
# runs only the weak test; the change sails through
python3 -m pytest -q tests/test_scale.py # UNPROVEN (previously SAFE)
```
Every changed statement executed in the second run, so coverage alone cannot
tell the two apart. The scope of the run is what distinguishes them.
Refactron classifies your command into three buckets, and reports the result on
`testScope`:
| Scope | Meaning | Can be SAFE |
| ---------- | -------------------------------------------------------------------- | ----------- |
| `full` | Parsed, and names no filter | yes |
| `narrowed` | Names a path, node id, `-k`, `-m`, `-t`, `--onlyChanged`, … | no |
| `unknown` | Not recognised (`make test`, `npm test`, a flag on a wrapper script) | yes |
**Naming a directory counts as narrowing**, including `pytest tests/`. Tests can
live outside any single directory, so Refactron cannot treat that as the whole
suite without risking a false `SAFE`. Run the bare command (`pytest -q`) to get a
`SAFE`-eligible verdict. `pytest .` is fine: `.` collects from the root, so it is
never narrower than the default.
For `unittest` the canonical whole-suite form is different, and Refactron treats
it accordingly: `python3 -m unittest discover -s tests` is `full`. Bare `discover`
starts from the current directory, so pointing it at the test directory is how a
complete unittest run is normally written, not a narrowing of one. Naming a
module, class or method (`python3 -m unittest tests.test_scale`) is `narrowed`.
**A run that executes no tests is narrowing too.** `--collect-only` and `--help`
exit successfully while selecting zero tests, and they still import your test
modules, so coverage can mark module-level changed lines as executed. Those are
classified `narrowed`.
`unknown` does not floor the verdict. Most commands that land there are a full
suite carrying a flag we do not recognise (`pytest --doctest-modules`), and
refusing `SAFE` for every unrecognised plugin flag would be worse than the gap it
closes. The CLI says so explicitly rather than staying quiet:
```
note: could not determine whether the test command runs the whole suite
(make is not a recognised test runner, so the scope is unknown);
the verdict assumes it does.
```
Refactron recognises `pytest`, `unittest`, `vitest` and `jest` by name, so
`python -m unittest tests.test_foo` and `python3 tests/runtests.py auth` are both
`narrowed`. A **flag** on an unrecognised script stays `unknown` and is not
floored: `python3 tests/runtests.py --parallel 4` could be a whole suite, and we
do not know that flag well enough to guess.
An exported `PYTEST_ADDOPTS` (or `VITEST_ADDOPTS` / `JEST_ADDOPTS`) **is** seen:
its value is scanned with the same rules as the command line, and it is checked
against the runner that variable actually feeds, so a `PYTEST_ADDOPTS` left in
your shell will not floor a vitest project.
Narrowing configured in your **pytest** configuration is also seen. `addopts`
and `testpaths` are read from `pytest.ini`, `tox.ini`, `setup.cfg` and
`pyproject.toml`, and `addopts` is scanned with the same rules as the command
line — so `addopts = -q --strict-markers` stays `full`, while `addopts = -k foo`
does not. `testpaths` always counts as narrowing: it restricts collection, and
whether the restriction excludes anything cannot be known without collecting.
Only the repository root is read. pytest walks up to find its rootdir, but your
suite runs inside an isolated copy whose parent is a temporary directory, so a
config file above your repository does not reach the run being judged.
A vitest `include` or a jest `testMatch` is still **not** seen. Those are
JavaScript and would have to be executed rather than parsed. In practice they
cannot reach a false `SAFE` today, because coverage is Python-only and a
JavaScript or TypeScript change already caps at `UNPROVEN`.
`full` therefore means "no filter was found in the command, the environment, or
your pytest config". It is still not a proof that the whole suite ran: a runner
plugin or a `conftest.py` can deselect tests in ways nothing here inspects.
The `PYTHONPATH=` prefix recommended below is **not** narrowing.
`PYTHONPATH=. python3 -m pytest -q` classifies as `full`.
## Make sure the tests run the code being verified
Refactron verifies a change in an isolated copy of your project. If your tests import the package from somewhere else, they will exercise the original code and the run proves nothing about your change.
The common cause is a project installed into the environment, including an editable install (`pip install -e .`), because `import yourpackage` then resolves to the installed location rather than the copy under verification. We hit this on Django: the same diff read `UNPROVEN` when the tests loaded the installed copy, and `UNSAFE` (correctly, since the auth suite catches the change) once the verified copy came first on `sys.path`.
Refactron detects the case rather than guessing: if a changed file is never even measured by coverage, the verdict reports that coverage could not be determined instead of claiming the code is untested, and the report carries the reason and this remedy in `coverage.unknownReason`.
To get a real verdict, make the verified tree win on `sys.path`. Either export `PYTHONPATH=.` before you run Refactron, or prefix the test command itself:
```bash theme={null}
PYTHONPATH=. python3 -m pytest -q # or PYTHONPATH=src for a src layout
```
Both forms work from the CLI. Over MCP only the prefix form is available, because the client spawns `refactron-mcp` itself and there is no shell of yours to export into. Relative paths are resolved from the copy being verified, which is what you want here.
Write the command in **module form** (`python3 -m pytest`) rather than as a bare console script (`pytest`). Coverage has to run the same program the test gate ran, and a console script is only runnable under coverage when it resolves to a Python file. It does not on Windows, where console scripts are native `.exe` launchers, nor under pyenv, asdf or nix, which install shell shims. Refactron declines to measure rather than measure a different program, so a bare console script reports that coverage could not be determined on those setups. Module form is measurable everywhere.
## Exit codes
The verdict maps to a process exit code so it can gate CI directly:
| Verdict | Exit code |
| ---------- | --------- |
| `SAFE` | `0` |
| `UNPROVEN` | `0` |
| `UNSAFE` | `1` |
Both `SAFE` and `UNPROVEN` pass, so a green suite on untested lines never silently blocks a merge. To fail CI on `UNPROVEN` too, read the `verdict` field from the [JSON report](/verification/verify-diff#the-json-report) and decide for yourself.
Bad input (a diff that doesn't apply, a missing flag) exits `2`; an unauthenticated CLI run exits
`7`. Those are operational errors, not verdicts.
# Verify a diff
Source: https://docs.refactron.dev/verification/verify-diff
Prove a change preserves the behavior your tests check, or find out it can't be proven. Refactron runs your real tests in an isolated shadow tree and returns SAFE, UNSAFE, or UNPROVEN.
`verify-diff` takes a change (your AI agent's, a codemod's, or your own) and answers one question: **did it preserve the behavior your tests check, and can you prove it?**
Refactron applies the diff in an isolated shadow tree, runs the syntax, imports, and test gates against it, checks whether your tests actually exercise the changed statements, and returns one of three verdicts. Your working tree is never touched.
## The verdict, in one line
| Verdict | Meaning | Exit |
| ---------- | ------------------------------------------------------------------------------------ | ---- |
| `SAFE` | Every gate passed **and** your tests exercised every coverable changed statement. | `0` |
| `UNSAFE` | A gate failed: the change broke something. | `1` |
| `UNPROVEN` | Tests pass, but the changed code isn't exercised (or coverage couldn't be assessed). | `0` |
`UNPROVEN` is the honest verdict. "Tests pass" is not the same as "this change is proven safe": if nothing runs the lines you changed, a green suite proves nothing about them. Refactron says so out loud and tells you which test to add. See [Verdicts](/verification/verdicts) for the full model.
Coverage is **Python-only** (via `coverage.py`). A TypeScript or mixed-language diff can never
earn `SAFE` today; it returns `UNPROVEN` with the reason "coverage of the changed code could not
be determined." The gates still run; only the coverage half is Python-only. Coverage also cannot
see code that runs only in a subprocess, so a change exercised solely through `subprocess.run` or
a `multiprocessing` worker reads as `UNPROVEN` until in-process subprocess coverage lands.
If your project is installed into the environment (an editable `pip install -e .` counts), your tests may import that copy instead of the one being verified, in which case the run proves nothing about your change. Put the verified tree first on `sys.path` by prefixing the test command: `PYTHONPATH=. python3 -m pytest -q`, or `PYTHONPATH=src` for a src layout. Use module form rather than a bare `pytest`, which cannot always be measured. See [Make sure the tests run the code being verified](/verification/verdicts#make-sure-the-tests-run-the-code-being-verified).
## Install
Requires Node.js ≥ 18, plus Python 3.8+ with `coverage.py` for the coverage half.
```bash theme={null}
npm install -g refactron
```
That puts `refactron` (and `refactron-mcp`) on your `PATH`. To skip the install, run `npx refactron verify-diff ...` instead.
`pip install refactron==0.3.0` installs a thin `refactron` shim that shells out to the npm CLI. It
is not a Node-free path: you still need Node.js ≥ 18 **and** `npm install -g refactron`. When the
npm CLI is missing, the shim prints the exact matching install command and exits non-zero.
### Build from source (contributors)
```bash theme={null}
git clone https://github.com/Refactron-ai/refactron
cd refactron
npm install
npm run build
```
The CLI is then `node dist/cli/index.js `. Substitute that for `refactron` in every example below.
## Authenticate
`verify-diff` is auth-gated. Log in once on this machine:
```bash theme={null}
refactron login
```
In CI, skip the browser and export a token instead:
```bash theme={null}
export REFACTRON_TOKEN=refactron_live_...
```
An unauthenticated run exits `7`.
## Run it
```bash theme={null}
refactron verify-diff [repoRoot] --diff [flags]
```
`repoRoot` defaults to `.`. The `--diff` file is a standard unified (git) diff.
```bash theme={null}
# A change your agent produced, saved to change.diff:
refactron verify-diff . --diff change.diff
```
Output is the verdict, its reason, and (for `UNPROVEN` by lack of coverage) the uncovered lines:
```text theme={null}
[UNPROVEN] Tests pass, but the changed code is not exercised by any test.
uncovered: calc.py:14
```
### Flags
| Flag | Type | Default | Description |
| ------------------ | ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--diff ` | path | none | Required. The unified diff to verify. |
| `--test-cmd ` | string | auto-detect | Override the test command (e.g. `"python3 -m pytest -q"`). Drives both the test gate and the coverage run. |
| `--mutate` | bool | false | Deep check (Python, slower): mutate the changed statements and rerun the suite; a surviving mutant caps the verdict at `UNPROVEN`. Downgrade-only. See [Verdicts](/verification/verdicts#mutate-was-the-change-actually-asserted). |
| `--flaky-check` | bool | false | Deep check (Python, slower): rerun a would-be-`SAFE` suite K times under a varied `PYTHONHASHSEED`; a test whose outcome varies caps the verdict at `UNPROVEN`. Downgrade-only. See [Verdicts](/verification/verdicts#flaky-check-was-the-green-stable-or-lucky). |
| `--json` | bool | false | Emit the full report as JSON instead of the one-line verdict. |
### Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0` | `SAFE` or `UNPROVEN`. |
| `1` | `UNSAFE`: a gate failed. A tests-gate failure prints the failing-test tail; a syntax or imports failure prints that gate's reason (the offending file or the unresolved import). |
| `2` | Bad input: unknown flag, missing `--diff`, the diff didn't apply (e.g. `diff did not apply to calc.py (stale base?)`), or the diff uses an operation verify-diff does not support yet (see [Unsupported operations](#unsupported-operations)). |
| `7` | Not authenticated. |
## Unsupported operations
v1 verifies content edits. A diff that deletes, renames, or copies a file, or changes a binary file, is refused with exit `2` rather than partially verified. This is deliberate: a diff that deleted a module while also making one innocuous edit once verified `SAFE`, yet applying it broke every import in the package. A partial verdict on the verifiable half must never read as a verdict on the whole diff.
| Operation | Message |
| ---------------- | ---------------------------------------------------------------------------------------- |
| File deletion | `diff deletes ; file deletions are not supported yet, verify that change manually` |
| File rename | `diff renames to ; renames are not supported yet` |
| File copy | `diff copies to ; copies are not supported yet` |
| Binary only | `diff contains only binary changes; nothing verifiable` |
| Binary plus text | `diff contains binary changes alongside text edits; binary changes cannot be verified` |
Detection is belt and braces: both the parsed diff and a raw scan of the diff text are checked, so a pure rename or deletion that the diff parser drops entirely is still caught. Full deletion and rename support is planned; until then, verify those changes manually.
## Test files touched
When a diff changes files that look like tests (a `tests/` or `test/` path segment, `test_*.py`, `*_test.py`, `conftest.py`, `*.test.ts`, or `*.spec.ts`), the human output prints one advisory line and the JSON report carries the list under `testFilesChanged`:
```text theme={null}
[SAFE] Tests pass and the changed code is covered.
note: this diff modifies test files (1): tests/test_calc.py
```
This is a note, not a verdict change. A green verdict on a diff that also weakens its own tests is still green; the note is there so you look before you trust it.
## The JSON report
`--json` emits a reproducible record of the verdict: the same structure the [MCP tool](/mcp/tool-reference) returns:
```json theme={null}
{
"reportVersion": 1,
"engineVersion": "0.4.0",
"verdict": "UNPROVEN",
"gates": {
"syntax": { "passed": true, "durationMs": 12 },
"imports": { "passed": true, "durationMs": 9 },
"tests": { "passed": true, "durationMs": 3140 }
},
"changedFiles": ["calc.py"],
"testFilesChanged": [],
"coverage": {
"tool": "coverage.py",
"changedLinesCovered": false,
"changedStatements": { "total": 3, "covered": 2 },
"uncovered": [{ "file": "calc.py", "line": 14 }],
"filesWithUncovered": 1,
"inertOnlyFiles": []
},
"reason": "Tests pass, but the changed code is not exercised by any test.",
"missingTests": [{ "file": "calc.py", "hint": "add a test exercising calc.py:14" }]
}
```
* `reportVersion`: schema version of this report. The shape below is a public contract; the version tells a consumer that stores reports which shape it is holding.
* `engineVersion`: the version of Refactron that produced this report. The two are not interchangeable and you will want both if you keep reports as history. `reportVersion` says which **shape** you are holding. `engineVersion` says which **rules** produced the verdict, which matters because what earns a `SAFE` has changed without the shape changing: 0.4.1 stopped a narrowed `--test-cmd` from earning `SAFE`, and started requiring every coverable changed statement to have run. Absent on reports written before 0.4.1.
* `gates`: each gate's `passed` flag and wall-clock. A failed gate also carries a `blockingReason`.
* `testFilesChanged`: changed files matching test conventions. A note that the diff touched tests, never a verdict input.
* `coverage.tool`: `coverage.py` when coverage was assessed, `none` when it couldn't be (non-Python diff, or no `coverage.py` installed).
* `coverage.uncovered`: one entry per unexercised **statement**, at the statement's first line, mapped from the changed line by AST containment. A multi-line statement reports once rather than once per wrapped line. Present on `SAFE` too: a `SAFE` can still carry statements coverage.py **excluded**, which no test could reach, and hiding them would make the verdict read stronger than it is. An entry marked `"excluded": true` sits in a `# pragma: no cover` or `if TYPE_CHECKING:` block that no test can reach.
* `coverage.changedStatements`: `{ total, covered }` across the whole diff, so you can read the ratio ("12 of 40 changed statements exercised") rather than only the boolean.
* `coverage.filesWithUncovered`: distinct files with at least one uncovered statement, counted **before** the cap.
* `coverage.inertOnlyFiles` / `coverage.removalOnlyFiles`: changed files with nothing for coverage to attest, because their added lines are all blank/comment lines, or because the file only had lines removed.
* `missingTests`: present on `UNPROVEN`-by-coverage, a concrete hint per uncovered statement, **capped at 50**. `coverage.uncoveredTruncated` and `missingTestsTruncated` (`{ shown, total }`) appear whenever a list was capped, so a truncated report never reads as a complete one.
## Verify in CI
Run `verify-diff` on a pull request's diff and let the exit code gate the merge. `UNSAFE` returns `1` and fails the job; `SAFE` and `UNPROVEN` return `0`.
```bash theme={null}
git diff origin/main...HEAD > pr.diff
refactron verify-diff . --diff pr.diff --test-cmd "python3 -m pytest -q"
```
`verify-diff` has no `--fail-on-unproven` flag. To fail CI on `UNPROVEN`, read the `verdict` field from `--json` output and set the exit code yourself; the process exits `0` for `UNPROVEN` by design, so a green suite on untested lines never silently blocks a merge unless you choose to.
## Next
The three verdicts, coverage fusion, and the Python-only limitation in depth.
Give your AI agent the same gate as a `verify_change` tool call.
How the shadow tree and the three gates work.