A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client — as a library, a CLI, and an MCP server.
Project description
intact: Self-Healing File Edits for Agents
A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client — as a library, a CLI, and an MCP server.
[!NOTE] Genesis.
intactwas designed, built, reviewed, and shipped end-to-end by Claude (Fable 5) as an autonomous long-run engineering experiment — from mining two weeks of real agent-session ledgers for the failure class worth killing, through a fleet of builder/critic subagents and adversarial review rounds, to the benchmark, the extraction, and this release. The human operator set the charter, held the release gates, and found the name. The eval numbers below are measured on that same fleet's real failures — this package was built by the population it serves.
The problem, measured
An independent audit of two weeks of this fleet's own Claude Code session ledgers found that the single largest recurring cost line wasn't a wrong answer or a slow model — it was harness friction: an agent's builtin Edit tool failing an exact-string match against text that had drifted underneath it (formatter passes, a stale read, whitespace normalization), 99 instances totaling roughly 190K tokens over the window, each one paid as a full read-diff-retry round trip.
tact exists to kill that class of failure at the source: it heals what's mechanical (whitespace, Unicode punctuation, indentation shift, small textual drift) through a symbolic, self-reporting reconciliation ladder, and it refuses loudly — never silently — the moment a match is genuinely ambiguous or absent, because a wrong guess in a file edit is worse than an honest failure.
It ships as a library, a CLI, and an MCP server, so the same reconciliation core backs whichever front end a given agent harness actually calls.
Install
pip install intact
# or
uv add intact
The CLI and library work with no extras.
The MCP server needs the mcp extra:
uv add "intact[mcp]"
60-second quickstart
As a library
from tact import read_file, apply_edits, Edit
r = read_file('config.py')
apply_edits(
'config.py',
[Edit(old='DEBUG = True', new='DEBUG = False')],
expected_sha256=r.sha256, # advisory -- a mismatch heals, it never blocks
)
As a CLI
# a builtin exact-match Edit just failed -- heal it in one round trip
uv run tact heal config.py --old "DEBUG = True" --new "DEBUG = False"
# or drive the whole read -> edit cycle through tact directly
uv run tact --json edit config.py --old "DEBUG = True" --new "DEBUG = False"
As an MCP server
Register tact (adapt the project path for your own install):
uv run --extra mcp --project /path/to/tact tact-mcp
Then call tact_edit/tact_heal/tact_definition/… as MCP tools instead of the harness's builtin Read/Edit.
See docs/howto.md for per-harness registration snippets (Claude Code, Droid, Codex, Cursor).
The reconciliation ladder
find_block descends five rungs of mounting tolerance, stopping at the finest rung that yields a unique match:
| Rung | What it forgives | Example |
|---|---|---|
EXACT |
Nothing — verbatim line equality | old_string matches byte-for-byte |
CANON |
Unicode punctuation substitution | a curly "quote" pasted in for a straight one |
RSTRIP |
Trailing whitespace drift | a formatter stripped trailing spaces on save |
INDENT |
A uniform leading-whitespace shift | the block moved one indent level deeper |
FUZZY |
Small textual drift, bounded by per-line similarity | a stale line number after a nearby edit landed |
Two or more candidates at any rung is a structured ambiguity refusal (every candidate shown, resolved only by a --near hint); zero candidates at every rung is a structured no-match refusal.
Rungs 1-4 are true equivalence relations; rung 5 is a similarity ball, not transitive — which is exactly why ambiguity-handling concentrates there.
See docs/explanation.md for the full framing.
The numbers
Replayed against the fleet's own real edit-failure history (tact.eval, ledger-mined, no near hint supplied — any multi-candidate rung is a refusal, never a guess):
| Corpus | Cases | Healed | False heals | Correct refusals |
|---|---|---|---|---|
Public subset (this repo, tests/) |
59 | 43 | 0 | 1 |
| Full private fleet corpus (eval-of-record) | 111 | 68 | 0 | 1 |
The public subset is a privacy-reviewed 59-fixture slice of the full 121-fixture harvest (62 fixtures held back for operator-stack detail or private prose, not for reconciliation performance — see tests/fixtures/ledger_eval/README.md for the curation).
Cases that don't heal are mostly no_match, not misses: a large share are modified_since_read failures where the stored file snapshot legitimately predates the real edit, so a refusal there is reality reflected correctly, not a ladder weakness.
Features
check/edit— resolve-then-atomically-write a batch of edits, with a dry-run twin (check_edits) that previews without touching the file.apply_manytransactions — resolve every file in a batch before writing any of them; any failure blocks the whole transaction, nothing partial is ever written.heal— fix a failed builtinEdit(path, old_string, new_string)in one read-only round trip.undo— a content-addressed pre-image journal behind every write, with a redoableundoverb.- Durable anchors — capture a block's position so it survives a session/compaction boundary;
resolve_anchorre-finds it, biased by its own remembered index. delta/read_since— a cheap "what changed since this hash" re-read, backed by the undo journal's blob store.rename_symbol— LSP-computed cross-file renames, converted into oneapply_manytransaction (the one verb that is not LSP-optional).apply_patch— ingest a unified diff or a fenced old/new block pair straight into anEditbatch.- LSP navigation —
def/refs/hover/sym/diag, name-first, backed by a persistent, resilient pyrefly/ruff client.
Honest limits
tact matches line-by-line, so a paragraph reflow (a prose formatter that rewraps text across different line boundaries) is not a whitespace/indent variant it heals — expect a no-match refusal and a manual re-read there.
Ambiguity refusals need a --near hint or a more distinctive block; tact will not pick a near-tie for you.
rename_symbol is LSP-backed only — it never falls back to a textual find-replace, since that risks renaming the wrong binding, so it structurally refuses when no live LSP is available.
The undo journal keeps the newest 32 pre-images per file and is index-pruned, not globally garbage-collected — it is acceptable-loss state, not a full version history.
Provenance
The reconciliation ladder's canonicalization table and fuzzy-matching primitives in tact/reconcile.py are a port, with attribution, of parts of dirac (Apache-2.0, version 0.4.11) — see NOTICE and reconcile.py's own module docstring for exactly what was ported and how it was adapted.
License
Apache-2.0.
See LICENSE and NOTICE.
Versioning
This project follows workflow-gated semver: the minor version records the highest-completed py-workflow step (0.1.0-0.8.0), with 0.9.0 (beta) gated on >=9 external users and 1.0.0 on a maintainer social guarantee.
The current version 0.7.0 means gate 7 is the highest cleared.
See the normative rubric and the overview.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file intact-0.8.0.tar.gz.
File metadata
- Download URL: intact-0.8.0.tar.gz
- Upload date:
- Size: 82.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5138545c8ce85f627ce9aefa0c7eaa3befe3b30fb49b693f8a80213c74e306d
|
|
| MD5 |
71d0c744621a00f52d5a5e0988ac96e0
|
|
| BLAKE2b-256 |
80e5d75e756fd8651ec4e2a5f8b544ad2a14214b26fcb44abfb8f68fcad3347b
|
File details
Details for the file intact-0.8.0-py3-none-any.whl.
File metadata
- Download URL: intact-0.8.0-py3-none-any.whl
- Upload date:
- Size: 95.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.25 {"installer":{"name":"uv","version":"0.11.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e38b89010387ef385e2cfa245f458c61949a2eeaf68ff87145ac1af1d9b00cfe
|
|
| MD5 |
6020e5b0442fecbb5dff5a47271c08f0
|
|
| BLAKE2b-256 |
c81398999a6c927923488f11517779fa74ab1f768ab99f79789e881627c7a7f4
|