Metadata-Version: 2.3
Name: intact
Version: 0.8.0
Summary: 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.
Author: Robb Doering
Author-email: Robb Doering <robb@doering.ai>
Classifier: Programming Language :: Python :: 3.13
Classifier: Framework :: Pydantic :: 2
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.readthedocs.io
Provides-Extra: mcp
Description-Content-Type: text/markdown

# intact: _Self-Healing File Edits for Agents_

![Pipeline Status](https://img.shields.io/gitlab/pipeline-status/doering-ai/libs/tact?branch=main) ![Test Coverage](https://img.shields.io/gitlab/pipeline-coverage/doering-ai/libs/tact?branch=main) [![License](https://img.shields.io/gitlab/license/doering-ai/libs/tact)](/LICENSE) [![Documentation](https://app.readthedocs.org/projects/intact/badge)](https://intact.readthedocs.io) ![Python Version](https://img.shields.io/pypi/pyversions/intact) [![PyPI Wheel](https://img.shields.io/pypi/wheel/intact)](https://pypi.org/project/intact) [![PyPI Types](https://img.shields.io/pypi/types/intact)](https://pypi.org/project/intact)

[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![version](https://img.shields.io/badge/version-0.8.0-blue)](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) [![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json)](https://github.com/facebook/pyrefly) [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/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

```bash
pip install intact
# or
uv add intact
```

The CLI and library work with no extras.
The MCP server needs the `mcp` extra:

```bash
uv add "intact[mcp]"
```

## 60-second quickstart

### As a library

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

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

```bash
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 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`](https://github.com/dirac-run/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`](LICENSE) and [`NOTICE`](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](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) and the [overview](https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md).

[workflow-gated semver]: https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md
