Skip to main content
inTact logo

inTact: Self-Healing File Edits for Agents

pipeline status license PyPI version

PyPI - Python Version PyPI - Wheel PyPI - Types pre-commit pyrefly ruff

Left-rule blocks mark artificial prose; quotations retain their own attribution.

A formatter changes the indentation; the edit you prepared no longer matches. tact can 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 is tact.

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 check finds the block at INDENT and previews the replacement. The result reports would_apply=True; the file still contains return 41. Use edit in place of check when you're ready to write.

Use it from the CLI

Start with check when you want to inspect the match. heal gives a read-only recovery result for a failed exact edit; edit writes the replacement. These examples assume config.py contains DEBUG = 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 LINE to choose by proximity. That hint breaks ties; it cannot make absent text appear. Exit status 1 means a structured refusal.

Input is forgiving about shape and strict about meaning. Common option aliases (--old-string, --search), Codex's *** Begin Patch text, and diffs with miscounted hunk headers are accepted. Text whose newlines arrived as a literal backslash and n is 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 --help for 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:

Public replay: 42 of 59 fixtures uniquely resolved, 13 no-match refusals, 1 ambiguity refusal, and 3 unreplayable cases; zero false-heal candidates among seven checkable same-intent pairs.

The false-heal check found zero candidates among seven 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. It also misses a wrong write that overlaps its retry. Two such FUZZY heals passed it until 1.2.1 refused them, which is why 44 became 42. 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_edits resolves the whole batch before writing the file. Here, expected_sha256 reports drift without rejecting an otherwise valid reconciliation. For cross-file changes, apply_many resolves and stages every file before committing any; its required_sha256s check 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 mcp extra, 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, and tact_delta. It keeps its LSP client alive across navigation and rename calls. tact_edit also takes one flat edit (path, old_block, new_block) and common aliases such as file_path or old_string/new_string. An unknown argument or replace_all: true is 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.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for intact 1.2.1
File Size Uploaded Provenance Actions
intact-1.2.1.tar.gz 194.1 kB Without provenance Details for intact-1.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for intact 1.2.1
File Interpreter ABI Platform Actions
intact-1.2.1-py3-none-any.whl Python 3 none any Details for intact-1.2.1-py3-none-any.whl

Total release size: 405.3 kB

Release files / intact-1.2.1.tar.gz

Download URL intact-1.2.1.tar.gz
Size 194.1 kB
Tags Source
SHA-256 checksum
How to use checksums
10a2de9bae48b2e1ee0e915261bc9c2474e290504e524c3ab1284945913d236f
BLAKE2b-256 checksum
How to use checksums
16037c21d1914bc5edc5c13171971d10211c73b8eb8fe4e7e172dff19c343574
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.1-py3-none-any.whl

Download URL intact-1.2.1-py3-none-any.whl
Size 211.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
80b7fa81e446eb2a7b23fc0b49f62be2c562ac84ce4624a1c1f0ef0614fbdb87
BLAKE2b-256 checksum
How to use checksums
a9bd8d71782792c330c17438c60f5d5cd43f76bace8d869f5287d08a76fb9bf6
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.1

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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page