# How-to: task-oriented recipes ## Recover a failed builtin edit A harness's builtin `Edit(path, old_string, new_string)` just failed with a no-match error. ```bash uv run tact heal PATH --old "OLD_STRING" --new "NEW_STRING" ``` Use the *exact same* `old`/`new` triple the failed builtin call used. `heal` is read-only. It returns either a corrected match (rung reached, actual current lines, a ready-to-apply preview) or a structured refusal (ambiguous with candidates, or genuinely no match). On success, either apply the corrected edit yourself or call `tact edit`/`apply_edits` with the same arguments directly. Those direct calls heal internally and write in one step. ## Apply an LLM-emitted unified diff A model emitted a standard `diff -u`/`git diff`-shaped patch (not a builtin `Edit` call): ```bash uv run tact apply-patch patch.diff # write uv run tact apply-patch patch.diff --check # dry run: parse + resolve, write nothing ``` Or pipe it in: `cat patch.diff | uv run tact apply-patch`. `apply-patch` also accepts this package's own fenced `old`/`new` block convention. The convention uses a `File: path` line followed by fenced ```` ```old ````/```` ```new ```` pairs. It is useful when a model was asked to show before/after rather than reconstruct a diff header. Parsing is file-scoped. One file's hunks either all parse cleanly or that file gets a structured error. Other files in the same input are unaffected. Parsing never applies. The resulting edits still go through the same reconciliation ladder every other verb uses. ## Multi-file rename via LSP Renaming a symbol requires knowing which occurrences are *the same binding*. It is not a textual find-replace. A textual find-replace risks renaming an unrelated local or a string that happens to match: ```bash uv run tact rename PATH TARGET NEW_NAME ``` `TARGET` is a bare symbol name (resolved name-first) or a `LINE:CHARACTER` position. This is the one verb in the package that is not LSP-optional. It needs a live pyrefly server for the file's project root, found by walking up for `pyproject.toml`/`.git`. If no server is available, it structurally refuses and never falls back to text search-and-replace. The computed `WorkspaceEdit` is converted into one `apply_many` transaction, so a rename across several files either lands everywhere or nowhere. ## Anchor a block across sessions, then delta re-read Anchor a block of interest before a context compaction or session boundary: ```bash uv run tact anchor PATH --block "def target_function():" --name my-anchor ``` Later, in a new session after compaction, re-locate it: ```bash uv run tact resolve-anchor my-anchor ``` `resolve_anchor` short-circuits to the remembered position if the file's `sha256` hasn't moved at all, else re-runs the reconciliation ladder biased toward the anchor's own remembered line. To find out cheaply what changed in a file since a hash you already have (without re-sending the whole file): ```bash uv run tact delta PATH KNOWN_SHA256 ``` `delta` composes with the undo journal. If the journal still has the content behind `KNOWN_SHA256`, the result carries a real unified diff. If the baseline aged out of the journal's ring or was never a tact-managed write, the result degrades honestly. The result includes the full current content plus a note that the baseline wasn't recoverable. It never returns a silent empty diff. ## Fleet-native routing `tact hook` is a JSON-over-stdin adapter for the five hook-bearing agent CLIs (Claude Code, Codex, Factory Droid, Gemini, Kimi). It reads only the tool name, working directory, and target path. It neither needs nor retains content. The default posture is **butler, not bouncer**. In `warn` mode, the native edit proceeds and records the route without repeating standing advice in the agent context. The agent keeps a harness's native rendering for the common case. It reaches for tact when an edit actually drifts, or via `tact_heal` after a builtin `Edit` fails. Switch to `deny` only when you want every eligible edit rerouted through tact unconditionally. ```bash # default: native edit proceeds; the route is recorded without added context cat native-hook-input.json | tact hook --harness claude # --mode warn is the default cat native-hook-input.json | tact hook --harness codex --mode warn # stricter: refuse the builtin edit and point at tact cat native-hook-input.json | tact hook --harness droid --mode deny cat native-hook-input.json | tact hook --harness gemini --mode deny # observe only, no context added (rollout dry-run) cat native-hook-input.json | tact hook --harness claude --mode shadow ``` The eligibility boundary is deliberately narrow: - existing ordinary files route to tact; - new files and notebooks stay with harness-native tools; - unrecognized tools and malformed payloads fail open, as does any path tact cannot normalize; - the hook never reads file content. For Claude Code, Codex, and Factory Droid, add this command to the native `PreToolUse` group for their edit tools. For Gemini, use `BeforeTool` and `write_file|replace`: ```json { "hooksConfig": {"enabled": true}, "hooks": { "BeforeTool": [ { "matcher": "write_file|replace", "hooks": [ {"type": "command", "command": "tact hook --harness gemini --mode warn"} ] } ] } } ``` A `warn`/`shadow` fleet rolls back to the bare native editor trivially. Drop the hook, and nothing changes about how edits land. A `deny` fleet depends on the agent actually following the redirect, so prefer `warn` unless you have a reason to force the issue. ## Native-style rendering for tact tools A harness renders its *own* builtin `Read`/`Edit` with bespoke UI that no MCP mechanism can opt into. That rendering lives in the client, not the protocol. What the protocol *does* offer is the **MCP Apps extension** (`io.modelcontextprotocol/ui`), which `tact-mcp` wires up so tact tools render "something akin to native" in the hosts that support it. ### GUI hosts (Claude.ai, Claude Desktop, Cursor) `tact-mcp` registers one inline widget (`ui://tact/view.html`) and points every tool at it. A GUI host that advertises the UI extension fetches the widget and renders it in a sandboxed iframe. The host pushes each tool result's `structuredContent` into it. The widget shows a lightly-syntax-highlighted file view for `tact_read`, a coloured unified-diff view for `tact_check`/`tact_heal`/`tact_delta`, and a key/value outcome/outline table. The model-facing text stays compact. The host owns whether and how it retains `structuredContent`. MCP does not guarantee that widget data costs zero context tokens. Terminal hosts (Claude Code, Codex CLI, Gemini CLI, Droid) do not fetch the widget. Routine all-EXACT `tact_edit`, `tact_check`, and `tact_apply_patch` successes return a compact text receipt by default. Request `detail="full"` when a consumer needs the former JSON/data shape. Healed or ambiguity-resolved matches, refusals, drift, and new diagnostics always retain full evidence. No configuration is needed beyond the MCP wiring above. The host negotiates the extension automatically at connect time. ### Terminal cosmetic (optional, opt-in) Claude Code renders ANSI inside MCP tool output, but some other terminals strip it. On a compatible terminal, set `TACT_MCP_ANSI=1` to prefix non-compact results with a single jade-coloured status word. This is a cosmetic touch, not a re-render: ```bash TACT_MCP_ANSI=1 uv run --extra mcp --project /path/to/tact tact-mcp ``` It is off by default so it can never pollute a model's context with literal escape codes in a host that strips ANSI. The honest ceiling in any terminal is that the *result* can get a coloured status word. The client renders the *call* (the grey parameter block), which has no server-side override. The butler-mode `warn` routing above is the practical mitigation. It lets the agent keep using the native, pretty-rendered `Edit` for the common case. It invokes tact, with its MCP rendering, only when an edit drifts or fails. Gemini CLI versions before `0.26.0` do not execute runtime hooks. `tact doctor` reports the installed surfaces. The opt-in live suite makes the compatibility floor observable rather than silently treating a projected-but-dormant hook as healthy. ```bash # ordinary suite: every live canary is skipped uv run pytest # all installed harnesses; real model calls, so explicit and billable uv run pytest --run-live-harness -m live_harness -vv # one harness, or an ephemeral known hook-capable Gemini build TACT_LIVE_HARNESS=codex uv run pytest --run-live-harness -m live_harness -x -vv TACT_LIVE_HARNESS=gemini \ TACT_GEMINI_COMMAND='bunx --bun @google/gemini-cli@0.26.0' \ uv run pytest --run-live-harness -m live_harness -x -vv ``` ## Diagnose a fleet install ```bash tact doctor tact --json doctor ``` The doctor checks the Python and CLI surfaces, the optional MCP extra, the state directory, and telemetry readability. JSON output is stable enough for a fleet preflight. Human output keeps the authoritative path and failure detail visible. ## Wire the MCP server into an agent harness The server is one stdio process: `uv run --extra mcp --project /path/to/tact tact-mcp`. `--extra mcp` is required. The server imports `fastmcp`, an optional dependency. ### Claude Code Add a `tact` entry to your MCP config (e.g. `.mcp.json`): ```json { "mcpServers": { "tact": { "type": "stdio", "command": "uv", "args": ["run", "--extra", "mcp", "--project", "/path/to/tact", "tact-mcp"] } } } ``` ### Factory Droid Merge the same server block into `~/.factory/mcp.json`'s `mcpServers` object. ### Codex CLI Add to `~/.codex/config.toml`: ```toml [mcp_servers."tact"] command = "uv" args = ["run", "--extra", "mcp", "--project", "/path/to/tact", "tact-mcp"] ``` ### Cursor Add to `~/.cursor/mcp.json`'s `mcpServers` object (same shape as the Claude Code block above). No `env` block is needed for any harness. `tact-mcp` reads no environment secrets.