Skip to main content

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

Pipeline Status Test Coverage License Documentation Python Version PyPI Wheel PyPI Types

pre-commit version pyrefly ruff

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. intact was 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_many transactions — 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 builtin Edit(path, old_string, new_string) in one read-only round trip.
  • undo — a content-addressed pre-image journal behind every write, with a redoable undo verb.
  • Durable anchors — capture a block's position so it survives a session/compaction boundary; resolve_anchor re-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 one apply_many transaction (the one verb that is not LSP-optional).
  • apply_patch — ingest a unified diff or a fenced old/new block pair straight into an Edit batch.
  • LSP navigationdef/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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

intact-0.8.0.tar.gz (82.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

intact-0.8.0-py3-none-any.whl (95.2 kB view details)

Uploaded Python 3

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

Hashes for intact-0.8.0.tar.gz
Algorithm Hash digest
SHA256 a5138545c8ce85f627ce9aefa0c7eaa3befe3b30fb49b693f8a80213c74e306d
MD5 71d0c744621a00f52d5a5e0988ac96e0
BLAKE2b-256 80e5d75e756fd8651ec4e2a5f8b544ad2a14214b26fcb44abfb8f68fcad3347b

See more details on using hashes here.

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

Hashes for intact-0.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e38b89010387ef385e2cfa245f458c61949a2eeaf68ff87145ac1af1d9b00cfe
MD5 6020e5b0442fecbb5dff5a47271c08f0
BLAKE2b-256 c81398999a6c927923488f11517779fa74ab1f768ab99f79789e881627c7a7f4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page