Left-rule blocks mark artificial prose; quotations retain their own attribution.
A formatter changes the indentation; the edit you prepared no longer matches.
tactcan still find it. It works through seven increasingly permissive kinds of text match, reports the one it used, and refuses unresolved ambiguity. It reconciles text; deciding whether the edit is a good idea is still your job.
Documentation: intact.docs.doering.ai
Installation
Python 3.13+. The package is
intact; the command istact.
uv tool install intact
# or, in your Python environment:
python -m pip install intact
The core has no runtime dependencies. For the optional MCP server, use
"intact[mcp]"in either command.
Preview an edit
Save this small example as
answer.py:
def answer():
return 41
tact check answer.py --old 'return 41' --new 'return 42'
The old text omits the file's indentation, but
checkfinds the block atINDENTand previews the replacement. The result reportswould_apply=True; the file still containsreturn 41. Useeditin place ofcheckwhen you're ready to write.
Use it from the CLI
Start with
checkwhen you want to inspect the match.healgives a read-only recovery result for a failed exact edit;editwrites the replacement. These examples assumeconfig.pycontainsDEBUG = True(possibly with changed whitespace).
# Inspect a proposed match without writing.
tact check config.py --old "DEBUG = True" --new "DEBUG = False"
# Get a read-only recovery result for a previously failed exact edit.
tact heal config.py --old "DEBUG = True" --new "DEBUG = False"
# Machine-readable output and a single atomic write.
tact --json edit config.py --old "DEBUG = True" --new "DEBUG = False"
# Multi-line blocks without shell quoting: read them from files, or pipe a JSON batch.
tact edit config.py --old-file old.txt --new-file new.txt
echo '[{"old_block": "DEBUG = True", "new_block": "DEBUG = False"}]' | tact edit config.py
# A patch in any common dialect: *** Begin Patch, unified diff, SEARCH/REPLACE, or fenced blocks.
tact apply-patch change.patch
Each result reports the match or refusal. If several places match, inspect the candidates before supplying
--near LINEto choose by proximity. That hint breaks ties; it cannot make absent text appear. Exit status1means a structured refusal.Input is forgiving about shape and strict about meaning. Common option aliases (
--old-string,--search), Codex's*** Begin Patchtext, and diffs with miscounted hunk headers are accepted. Text whose newlines arrived as a literal backslash andnis decoded only when it is not in the file as written and the decoded text then matches, and the result says so. A shape that two readings fit, such as a patch mixing two dialects, is refused by name. Patch text that parses to nothing is a refusal, never a success.The CLI also handles patches, undo/history, durable anchors, drift reads, and LSP-backed navigation and rename. Run
tact --helpfor the available commands.
What the public replay shows
The committed public fixture set contains 59 historical edit failures, reviewed for privacy: 56 have enough evidence to replay, and 3 do not. The current replay reports:
The false-heal check found zero candidates among nine checkable same-intent pairs. That isn't a general error rate: the check needs a later, resolvable follow-up similar enough to plausibly represent the same edit. See the fixture provenance and evaluation test.
The shape-map methodology and full PCA triptych explore the same fixtures. Nearby points have similar edit shapes; proximity is not evidence that an edit was correct.
Use it from Python
from tact import Edit, apply_edits, read_file
snapshot = read_file('config.py')
result = apply_edits(
'config.py',
[Edit(old_block='DEBUG = True', new_block='DEBUG = False')],
expected_sha256=snapshot.sha256,
)
print(result.results[0].rung)
apply_editsresolves the whole batch before writing the file. Here,expected_sha256reports drift without rejecting an otherwise valid reconciliation. For cross-file changes,apply_manyresolves and stages every file before committing any; itsrequired_sha256scheck refuses a stale snapshot. The distinction matters: expected hashes are advisory, required hashes are gates.Multi-file commits still use separate renames. A crash or write failure between them can leave a partial batch; resolve-first is not filesystem-wide atomicity.
Use it through MCP
After installing the
mcpextra, start the server with:
tact-mcp
The server exposes the same core through tools such as
tact_read,tact_edit,tact_check,tact_heal,tact_anchor, andtact_delta. It keeps its LSP client alive across navigation and rename calls.tact_editalso takes one flat edit (path,old_block,new_block) and common aliases such asfile_pathorold_string/new_string. An unknown argument orreplace_all: trueis refused with the accepted shapes, never silently dropped. See the MCP setup guide for host configuration.
How matching stays safe
The ladder tries exact text, normalized Unicode, trailing-space-insensitive text, indentation shifts, exact substrings, reflowed prose, then bounded fuzzy matching. At each rung, no candidates means try the next; one candidate ends the search; several require a proximity hint or produce an ambiguity refusal. If the whole ladder finds nothing, the result is a no-match refusal.
Matching is only part of the write checks: overlapping edits and unsafe replacements can still be refused. Patch ingestion and multi-edit batches use the same resolve-first machinery. The undo journal keeps pre-images for convenience; keep using version control. LSP-backed rename refuses when it cannot establish bindings, with no textual fallback.
Advanced integration
Optional agent-harness routing directs eligible editor calls through Tact and checks installation readiness. You don't need the hooks to use the CLI, library, or MCP server.
License and provenance
Apache-2.0.
See LICENSE and NOTICE.
The reconciliation ladder's canonicalization and fuzzy primitives adapt attributed Apache-2.0 code from dirac; the original SUBLINE and REFLOW rungs are documented in the design explanation.
Metadata
Release files for intact 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| intact-1.2.0.tar.gz | 192.9 kB | Without provenance | Details for intact-1.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| intact-1.2.0-py3-none-any.whl | Python 3 | none | any | Details for intact-1.2.0-py3-none-any.whl |
Total release size: 402.8 kB
Release files / intact-1.2.0.tar.gz
| Download URL | intact-1.2.0.tar.gz |
|---|---|
| Size | 192.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c70b26274575bea25a9a87f5136e16afd16acfacaead72e7393bbb963fda0caa
|
|
BLAKE2b-256 checksum How to use checksums |
17f1cd744b503d92619f123f215d07042a3f098962357ede4573873c8e3cb941
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / intact-1.2.0-py3-none-any.whl
| Download URL | intact-1.2.0-py3-none-any.whl |
|---|---|
| Size | 209.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
93fa78e5d483602c9811229c7f597abe570b3d05bcac38929e07f9706540bfce
|
|
BLAKE2b-256 checksum How to use checksums |
d955fd7cbdae854c009c216539959105a38de200dcf8b0052c460db76fcf1e98
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for intact 1.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.