# Reference: verb ↔ CLI ↔ MCP ↔ library Every verb ultimately calls one library function. The CLI and MCP server are thin skins over the same core (`tact/cli.py` and `tact/mcp_server.py`). They handle argument parsing, dispatch, and rendering only. This table is the map. Each function's own docstring (linked via its module) is the deep, load-bearing reference. Read it before relying on an edge case not spelled out below. | Verb | CLI | MCP tool | Library call | One-line contract | | -------------- | --------------------------------------------- | --------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Read | `tact read PATH` | `tact_read` | `tact.read.read_file` | Content-hash dedup (`unchanged=True` via a `seen_hashes` set) + byte-capped read; the file is always stat'd/hashed in full even when content is truncated. | | Edit | `tact edit PATH --old --new` | `tact_edit` | `tact.apply.apply_edits` | Resolve every edit via the ladder, interval-check for overlap, splice descending, write atomically. `expected_sha256` is advisory — a mismatch heals, never blocks. | | Check | `tact check PATH --old --new` | `tact_check` | `tact.apply.check_edits` | Identical resolution pipeline to `edit`, writes nothing — a dry-run preview. | | Apply patch | `tact apply-patch [FILE\|-] [--check]` | `tact_apply_patch` | `tact.patch.parse_patch` → `apply_many`/`check_edits` | Parses a unified diff or fenced old/new blocks; writes resolve and stage all files before committing, while checks remain read-only. | | Heal | `tact heal PATH --old --new` | `tact_heal` | `tact.heal.heal` | Read-only fix for a failed builtin `Edit(path, old_string, new_string)`; returns a corrected match or a structured refusal, never writes. | | Undo | `tact undo PATH` | `tact_undo` | `tact.undo.undo` | Restores the newest journaled pre-image; a toggle of the most recent write, journaling the pre-undo state so undo is itself redoable. | | History | `tact history PATH` | *(not exposed)* | `tact.undo.history` | Lists a file's journaled pre-images, newest first. | | Definition | `tact def PATH TARGET` | `tact_definition` | `LspManager.definition` | Name-first (or `LINE:CHARACTER`) go-to-definition via pyrefly; `LspResult(ok=False)` on no live server. | | References | `tact refs PATH TARGET [--no-declaration]` | `tact_references` | `LspManager.references` | Same resolution, `references` LSP request. | | Hover | `tact hover PATH TARGET` | `tact_hover` | `LspManager.hover` | Type/docstring info at the resolved position. | | Symbols | `tact sym PATH` | `tact_symbols` | `tact.skeleton.skeleton` | LSP `documentSymbol` first; falls back to a dependency-free `ast` walk for `.py` files when no server is live; a structured "no skeleton available" result for anything else. | | Diagnostics | `tact diag PATH` | `tact_diagnostics` | `LspManager.diagnostics_for` | Every diagnostic currently known for the file, merged across pyrefly + ruff; `None` (not an empty tuple) means no live server. | | Rename | `tact rename PATH TARGET NEW_NAME` | `tact_rename` | `tact.rename.rename_symbol` | LSP-computed `WorkspaceEdit`, converted into one `apply_many` transaction. The one verb that is not LSP-optional — no textual fallback. | | Anchor | `tact anchor PATH --block TEXT [--name NAME]` | `tact_anchor` | `tact.anchor.anchor` / `save_anchor` | Captures a block's position + content for later re-finding; optionally persists it under a name. | | Resolve anchor | `tact resolve-anchor NAME` | `tact_resolve_anchor` | `tact.anchor.load_anchor` → `resolve_anchor` | Byte-identical short-circuit if the file's hash hasn't moved; otherwise re-runs the ladder biased toward the anchor's remembered index. | | Delta | `tact delta PATH SHA` | `tact_delta` | `tact.delta.read_since` | "What changed since this hash" — a real unified diff if the undo journal still has the baseline blob, else an honest full-content fallback with a note. | | Restart | `tact restart PATH` | `tact_restart` | `LspManager.restart` | Clears a project root's LSP blacklist and drops live servers, forcing a fresh spawn on next use. | | Doctor | `tact doctor` | *(not exposed)* | `tact.doctor.inspect_installation` | Reports CLI, optional MCP, state, and telemetry readiness in human or stable JSON form. | | Adoption | `tact adoption [--days N]` | *(not exposed)* | `tact.adoption.build_report` | Summarizes the event stream into an auditable adoption denominator: per-harness eligible counts, refusals, and hook p95. | | Hook route | `tact hook --harness H [--mode M]` | *(hook adapter)* | `tact.routing.route_hook` | Parses a native pre-edit payload and renders the selected harness's native response; `--mode` defaults to `warn`, and malformed or out-of-scope operations fail open. | ## Routing modes | Mode | Eligible existing-file edit | | ------------------ | --------------------------------------------------------------------------------------------------------------------- | | `shadow` | Observe and record; emit no intervention. | | `warn` *(default)* | Observe and record without injecting agent-facing context. | | `deny` | Refuse the builtin edit, state that it was not applied, name the tact CLI/MCP recovery action, and name the way back. | `route_hook` uses `warn` when no mode is given. The library is advisory by default, and only a caller's explicit `--mode deny` blocks anything. Routing is content-blind. Existing ordinary files are eligible. Creation, notebooks, unrecognized tools, and malformed payloads remain native. So does any payload tact cannot even normalize into a path: classification failures resolve to allow rather than propagating out of the hook. ## Exit codes (CLI only) | Code | Meaning | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | Success. | | `1` | A structured refusal the library itself reported (ambiguous match, no match, drift-blocked transaction, unavailable LSP capability) — the result is still printed to stdout, only the exit code marks it. | | `2` | A usage error — bad arguments, a missing/non-regular file, anything the library raises `FileNotFoundError`/`ValueError` for. | | `3` | An unexpected failure — any other exception `main()` didn't anticipate. Under `--json` the result is a structured error object naming the exception's type and message, so stdout is never empty; the traceback always goes to stderr, `--json` or not. | `--json` is accepted either before or after the verb (`tact --json edit ...` and `tact edit ... --json` are equivalent). The selected result, and nothing else, goes to stdout. Supplementary notes (such as a truncation notice or no-live-LSP note) go to stderr. ## MCP coverage gap `history`, `doctor`, and `adoption` are CLI-only today. The MCP server exposes no `tact_history`/`tact_doctor`/`tact_adoption` tools. This is a real gap, not a documentation oversight. Use the CLI or the library directly for those three verbs from an MCP-only harness until it closes. `hook` is deliberately absent from MCP. It is a harness adapter invoked by a hook runner, not a verb an agent calls. ## Deep reference Every module carries its full contract in its own docstring. Run `python -c "import tact.X; help(tact.X)"`, or read the source directly. Start with `tact/__init__.py`'s package docstring for the overall shape, then the module for the verb you're using.