Metadata-Version: 2.3
Name: intact
Version: 1.2.0
Summary: Reliable file edits for agents: reconcile drift, write only unique matches, and refuse ambiguity.
Author: Robb Doering
Author-email: Robb Doering <robb@doering.ai>
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Dist: fastmcp>=3.4.0 ; extra == 'mcp'
Requires-Python: >=3.13
Project-URL: Homepage, https://gitlab.com/doering-ai/libs/tact
Project-URL: Source, https://gitlab.com/doering-ai/libs/tact
Project-URL: Issues, https://gitlab.com/doering-ai/libs/tact/-/issues
Project-URL: Documentation, https://intact.docs.doering.ai/
Provides-Extra: mcp
Description-Content-Type: text/markdown

<!-- readme-header:begin -->

<div align="center">

<img src="assets/logo_512.png" alt="inTact logo" width="180">

# inTact: _Self-Healing File Edits for Agents_

<p align="center">
  <a href="https://gitlab.com/doering-ai/libs/tact/-/pipelines"><img src="https://img.shields.io/gitlab/pipeline-status/doering-ai%2Flibs/tact" alt="pipeline status"></a> <a href="https://gitlab.com/doering-ai/libs/tact/-/blob/main/LICENSE"><img src="https://img.shields.io/gitlab/license/doering-ai%2Flibs/tact" alt="license"></a> <a href="https://pypi.org/project/intact/"><img src="https://img.shields.io/pypi/v/intact" alt="PyPI version"></a>
</p>

<p align="center">
  <a href="https://pypi.org/project/intact/"><img src="https://img.shields.io/pypi/pyversions/intact" alt="PyPI - Python Version"></a> <a href="https://pypi.org/project/intact/"><img src="https://img.shields.io/pypi/wheel/intact" alt="PyPI - Wheel"></a> <a href="https://pypi.org/project/intact/"><img src="https://img.shields.io/pypi/types/intact" alt="PyPI - Types"></a> <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=precommit" alt="pre-commit"> <a href="https://github.com/facebook/pyrefly"><img src="https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json" alt="pyrefly"></a> <a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="ruff"></a>
</p>

</div>

<!-- readme-header:end -->

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

<blockquote class="artificial-prose">

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.

</blockquote>

**Documentation:** [intact.docs.doering.ai](https://intact.docs.doering.ai/)

## Installation

<blockquote class="artificial-prose">

Python 3.13+.
The package is `intact`; the command is `tact`.

</blockquote>

```bash
uv tool install intact
# or, in your Python environment:
python -m pip install intact
```

<blockquote class="artificial-prose">

The core has no runtime dependencies.
For the optional MCP server, use `"intact[mcp]"` in either command.

</blockquote>

## Preview an edit

<blockquote class="artificial-prose">

Save this small example as `answer.py`:

</blockquote>

```python
def answer():
    return 41
```

```bash
tact check answer.py --old 'return 41' --new 'return 42'
```

<blockquote class="artificial-prose">

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.

</blockquote>

## Use it from the CLI

<blockquote class="artificial-prose">

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).

</blockquote>

```bash
# 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
```

<blockquote class="artificial-prose">

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.

</blockquote>

## What the public replay shows

<blockquote class="artificial-prose">

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:

</blockquote>

![Public replay: 44 of 59 fixtures uniquely resolved, 11 no-match refusals, 1 ambiguity refusal, and 3 unreplayable cases; zero false-heal candidates among nine checkable same-intent pairs.](docs/_static/tact-evidence.svg)

<blockquote class="artificial-prose">

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](tests/fixtures/ledger_eval/README.md) and [evaluation test](tests/test_tact_ledger_eval.py).

</blockquote>

<blockquote class="artificial-prose">

The [shape-map methodology and full PCA triptych](https://intact.docs.doering.ai/error-cloud.html) explore the same fixtures.
Nearby points have similar edit shapes; proximity is not evidence that an edit was correct.

</blockquote>

## Use it from Python

```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)
```

<blockquote class="artificial-prose">

`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.

</blockquote>

## Use it through MCP

<blockquote class="artificial-prose">

After installing the `mcp` extra, start the server with:

</blockquote>

```bash
tact-mcp
```

<blockquote class="artificial-prose">

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](https://intact.docs.doering.ai/howto.html) for host configuration.

</blockquote>

## How matching stays safe

<blockquote class="artificial-prose">

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.

</blockquote>

## Advanced integration

<blockquote class="artificial-prose">

Optional [agent-harness routing](https://intact.docs.doering.ai/howto.html#fleet-native-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.

</blockquote>

## License and provenance

Apache-2.0.
See [LICENSE](LICENSE) and [NOTICE](NOTICE).
The reconciliation ladder's canonicalization and fuzzy primitives adapt attributed Apache-2.0 code from [dirac](https://github.com/dirac-run/dirac); the original `SUBLINE` and `REFLOW` rungs are documented in [the design explanation](https://intact.docs.doering.ai/explanation.html).
