33. AI agents
Working in a codebase isn't how coding agents usually work: there are no source files to edit, definitions are
found and changed through tcm, a change re-hashes what depends on it, and tests only run when something they
reach changed. tcm mcp teaches an agent that, as a Model Context Protocol
server its tools and instructions come from. Claude Code, Cursor, Codex and other MCP clients pick it up.
33.1 Setting it up
tcm init does it: it adds the server to the project's .mcp.json (tcm mcp), and writes a short section about
tcm into AGENTS.md for agents that read that instead. Neither is changed if it already mentions tcm. Elsewhere,
register tcm mcp (stdio) as an MCP server run in the codebase's directory.
33.2 What the agent gets
The server's instructions say how to work here: find code, tcm_edit it into a scratch file, edit that with the
agent's own file tools, tcm_check, tcm_update, fix what tcm_todo lists, tcm_test, giving each change a message saying what it's
for. They also say what pays
off in a codebase like this: keeping definitions out of cycles (saving warns about ones worth avoiding), letting
imports follow the code (tcm_patch, tcm_add, tcm_check and tcm_update import what the code uses, and
saving takes out imports nothing uses), changing together
what changes together (a class and what's typed with it, in one scratch file: each definition is saved with its
own names, and what the file imports that uses what it changes is re-derived with it), patching doc comments as tcm_view shows them, and viewing saved code again rather than
keeping copies of it in files. Each tool runs the tcm
command of the same name, so it does exactly what that command does at a terminal, and results say what to do
next where there's a next step (dependents to fix, a check to save). The quick ones (finding, viewing and editing
code, tcm_status, tcm_todo) run in a tcm the server keeps running, so they answer in milliseconds rather than
starting a tcm each; tests, running code, and a call while that one's busy each get a tcm of their own. A client that asks for progress hears each
line of a command's output as it comes (MCP progress notifications), so a long tcm_test shows its suites as
they finish; with background: true, tcm_test, tcm_run and tcm_cli start their command and answer at once,
and tcm_job says how it's going. A job is a process of its own, not the server's: it runs on when the server is
started again (a new tcm), and the new server follows it (below).
What the tools say is brief (TCM_BRIEF=1 for the commands they run), since an agent reads every line: a save
reports what changed (with a type only where it's new or changed), with the dependents it re-hashed and the definitions saved with it in a cycle as counts (tcm diff lists them), lint warnings as a count, a test run's passing tests as lists of names rather than a line each (the ones that ran
always; the cached ones too when you named tests or something failed, else counted), under the namespace they share,
and what failed as the start of its message and where it failed, with all of it in a file the report names, and a merge's changes by name and
count. A history entry lists what it edited, added and removed by name, and counts the definitions only re-hashed
because a dependency changed (all lists them); tcm_todo lists the first 20 dependents that need fixing (all
for every one). Progress notifications still carry every line as it comes.
tcm_status- where things stand: the branch (and, in a session, how many history entries and names it has that
maindoesn't), the codebase by namespace, scratch files holding unsaved work (and, in a session, the directory they're in), what needs fixing, other branches holding workmaindoesn't have, session directories left holding unsaved work, the mirror if there is one tcm_find- names, docs and signatures;
:<type>searches by type tcm_search- text in the code and pages, as find in files does: each line with it, by definition, numbered as
tcm_view'slinescounts; the text as written, or a regular expression written/…/, astcm_view'sgreptakes it (regexis the old way);undera namespace or a definition (finding code) tcm_view,tcm_docs- definitions with current names and where they run, as Markdown: a line with each one's names, hash and placement, then its code (or a file's content) in a highlighted block (
linesorgrepfor only some of a large one, numbered:grepis text as written,a|bincluded, or a regular expression written/…/;linesas120,120-180,120-or1-20,90-). A name that isn't there doesn't hide the others: it's said first, with the names meant most likely, or what a namespace holds (an error only if none are found); documentation, examples evaluated tcm_deps,tcm_dependents- what a definition uses, and what uses it
tcm_edit- definitions into a scratch file, its whole path and contents returned (the contents in a highlighted block); a scratch file holding unsaved work keeps it: the definitions are added to it. A conflicted name gets a file of its own, named after it (
lib.k.ts), holding its two sides merged from the merge base's, with conflict markers where both changed the same lines, and says what each side changed (and where) and whether the merge typechecks; one using another conflicted name is edited with it (namesboth, one file). Saved withtcm_update(thatfile) once the markers are out, it resolves the conflict, namedresolve <name> (merged)in history (merging) tcm_patch- a small change in one step: find-and-replace on a definition's code as
tcm_viewshows it, checked and saved astcm_updatedoes, the change shown; nothing is saved if an edit doesn't apply (what's nearest is said) or the result doesn't typecheck, a problem's line said astcm_viewnumbers it, the edits made. Imports follow the code; one that can't be found that way (an alias) is given inimports([{"name": "isImportDeclaration", "from": "typescript", "as": "ts_isImportDeclaration"}]), and goes if the code doesn't use it.patcheschanges several definitions, and pages, saved together as one update and one history entry, each edit matched against its definition astcm_viewshows it (each patch withimportsof its own). Inside a template literal,oldandneware as the source writes them, escapes and all (in JSON,"\\n"for the source's\n): anoldfound only with its escapes taken the other way (a line break for\n, a bare backtick for\`,${for\${) is matched so when that finds it once,newwritten alike, and said; anewthat would end or split the literal (a bare backtick or${) is refused, saying how to escape it. Code that doesn't parse is said by its first syntax error alone (bytcm_checkandtcm_updatetoo) tcm_add- code given in the call saved without a scratch file, into a namespace (
updateto replace definitions of the same names) tcm_at- which definition is at a line of a file (a stack trace's, a search result's), or every definition in a file with its lines; a view of another branch's definitions with
branch tcm_check,tcm_update- what saving a scratch file would do; saving it, and deleting it once saved (
keepto keep it).intofor a file kept as written, astcm_editsays. A definition saved at a conflicted name resolves the conflict to it (resolves), re-deriving what used either side's once tcm_todo- conflicts (each definition labelled with its side of the merge: ours or theirs, and its branch), dependents that stopped typechecking (the first 20, unless
all), and code using a version nothing names tcm_resolve- a conflicted name settled by keeping one definition whole:
sideoursortheirs, astcm_todolabels them (found as they are now, after what re-derived them since), or ahash tcm_test,tcm_run- tests (cached by hash,
namesfor only some,freshto run them all) and suites (cached by the code they ran,definitionsto leave them out,suitesfor only them;binruns them against another build of their command, as{"tcm": "<tcm.js>"}, or a kept build's name,{"tcm": "known-good"}: to see an end-to-end test fail on the tcm from before a fix); running a definition tcm_mv,tcm_rm,tcm_history- renaming, removing, history (the latest
nentries, 10 unless said; each named by what it changed, and listing it by name, the definitions only re-hashed because a dependency changed counted unlessall) tcm_merge- in a session, only when you've said to (
approved;branchfor an earlier session's): land its branch onmain, once the tests the change reaches pass, as one entry called by itsmessage(required: what the branch did and why, asmain's history will say it); it says what landed by name and count (tcm diffhas the code), as it does what mergingmainin did when that stops on a conflict (the conflicts, labelled by side, and what no longer typechecks), and of failing tests only the failures.backgroundruns it as a job (tcm_job), as the tests can outlast a call tcm_job- a command (or
tcm_merge) started in the background: how it's going (a test run: how many finished, and what failed), or how it ended;waitwaits for it to end, up to that many seconds;stopstops it, and what it runs. Without an id, the session's jobs, running or ended, those an earlier server started included (background jobs) tcm_cli- any other command (CLI); in a session,
["switch", "<branch>"]moves it onto that branch (below);liveruns the codebase's own tcm, for a codebase that is one (tcm's);cwdruns it in another directory, on the codebase there (tcm -C);buildruns it with a kept build (tcm --build:known-good,previous, an id) tcm_guide- longer explanations, by topic: the model, editing, testing, apps, packages and releases, mirrors, troubleshooting
The tools that change names (tcm_patch, tcm_update, tcm_add, tcm_mv, tcm_rm, tcm_resolve) take
message: what the change is for, a line saying what and why and more after a blank line if it needs it, kept with
the history entry it makes as its title (messages). tcm_cli ["describe", "<n>", "-m", "…"]
changes one afterwards.
The tools that only read (tcm_status, tcm_find, tcm_search, tcm_view, tcm_deps, tcm_dependents,
tcm_docs, tcm_todo, tcm_history) take branch: they read that branch (another session's, say) as it is, with
the CLI's --branch, and change nothing; the session stays on its own (below). The tools that
change things don't take it.
The tools that change code (tcm_edit, tcm_patch, tcm_check, tcm_update, tcm_add), tcm_test, and the
ones that read the codebase (tcm_status, tcm_find, tcm_search, tcm_view, tcm_deps, tcm_dependents,
tcm_docs, tcm_at, tcm_todo, tcm_history) take live too: in a codebase that holds a tcm (tcm's own), the call is answered by that tcm as it is now, rather than
the server's, the last released or verified: a server of its own (tcm run --bin tcm -- mcp) is asked it, so the
whole tool is the new one, what it says included. So a change to how tcm edits, to how a test run reports, to
what a tool shows, or to a tool, can be used before it's verified. With background, a live call is a job (tcm_job), that server the
job's own, what it prints coming as it goes; tcm_job doesn't take live, as a job is the session's whichever tcm
runs it, and what a live job says is the new tcm's already.
To try the branch's tcm somewhere else (a codebase made for a repro), tcm_cli with live and cwd: it runs
tcm run --bin tcm -- -C <dir> …, the session's tcm on the codebase in that directory, not on this one or the
session's branch.
To show a bug is in a tcm from before, or that a new test's case fails without the fix, compare with a kept build
(testing): known-good is main's last verified build, the one the
server runs, without the session's change; previous is the one before it. tcm_test's bin runs the end-to-end
tests against one ({"tcm": "known-good"}), and tcm_cli with build and cwd runs a command with it on a
repro's codebase (tcm --build previous -C <dir> …). tcm_cli ["builds"] lists them, and
["builds", "make", "<entry>"] makes one from any history entry.
33.3 Branches: sessions
An agent's work stays on a branch until you say to land it, as work on a git branch does. Each tcm mcp is a
session on a branch of its own: a new one (session/<date>-<id>), unless the agent's last session left one
holding its work (below), so agents can also work in one codebase at the same time. --branch agent/a names the
branch instead (made if it's new, gone on with if not), and --direct works on the codebase's own branch with no
session, changes landing as they're saved (--session, which used to ask for a session, is the default now). Two
servers never work on one branch: a server started with --branch that another agent's server is on says so and
stops. A session:
- works on its own branch (made from the codebase's own if it's new), whatever the others do: every command
it runs is on its branch (
TCM_BRANCHnames it, for the CLI too), andtcm switchthere moves the session, not the codebase (below). That includes a change to only a definition's comments or doc:mainreads it as before until it lands, and it's an entry in the session's history thattcm resetandtcm revertundo (history); - has its own scratch files,
#codebaseimports and definition views (src/, which its stack traces point into;main's.tcm/srcis never read or written for it), in.tcm/sessions/<branch>/, the branch's name with each character other than a letter, digit,_,.or-made_(session/2026-10-31-84f401's are in.tcm/sessions/session_2026-10-31-84f401/, astcm_statusand the agent's instructions say): a scratch file named by itself (scratch.ts) is there, one with a path is where the path says; - leaves the repository's files alone: a mirror follows the branch it was linked on
(
main), so a session's work reaches the files when it lands there; - lands with
tcm_merge, when you say so: the agent's instructions say to ask you once the change is done and tested.tcm_mergeistcm merge --squashonmain(landing): it mergesmaininto the session's branch (one that leaves conflicts, or dependents it broke, waits on the session's branch, with scratch files holding what to settle: the session fixes them, thentcm_mergeagain) and runs the tests the change reaches; if they pass, it lands the branch onmainas one history entry, squashed: its only parent ismain's, so the session's own entries (each step it took) stay on its branch, and pushingmainsends what the session ended with rather than every version along the way (unless it made releases: then its history comes too, as they're entries of it), and merges the landing into the session's branch, so work the session goes on with there lands next without conflicting with what it landed;tcm revertundoes the landing and nothing else. It lands nothing unless the agent says you told it to (approved: true): its description and the agent's instructions say not to otherwise. You can land a branch yourself, too:tcm merge <branch> --squashonmain; - says what it has that
maindoesn't:tcm statusin a session counts its branch's history entriesmaindoesn't have and the names they changed, or says there's nothing, and that it lands withtcm_mergewhen you say so; - goes on after a restart: a server the supervisor starts again (tcm replaced, or the server crashing) is on
the same branch. So is a new server for the same agent (its client reconnecting after a pause, the old server
stopped or killed): one started from the same directory (a worktree's, say: the directory the agent's client
runs it from, as the shell names it) goes on with the branch that directory's last session made or moved onto,
if that holds work
maindoesn't have (or a scratch file holding unsaved work) and no other server is on it. Its instructions say it's gone on with that work, and its scratch files are where they were, as are its background jobs: ones still running run on, andtcm_jobfollows them. The session's directory keeps which directory's it is (.tcm/sessions/<branch>/session.json). A server whose branch is deleted under it (by a tcm from before this) has it made again frommainrather than failing every call, and says so; - moves onto another branch without a restart:
tcm_cli ["switch", "<branch>"]in a session moves it, every tool and any server the supervisor starts again with it, onto a branch it owns (an earlier session's holding its work, whichtcm_statuslists), if no other session's server is on it. Not ontomain: work lands there withtcm_merge. The branch it leaves, if a server made it, goes ifmainhas all of it, as when a session stops; - reads other branches without moving: the tools that only read take
branch(tcm_viewwith"branch": "session/2026-10-31-84f401",tcm_history,tcm_status,tcm_todo, …), and the CLI--branch: another session's work (to check it, or settle a conflict with it) is read as that branch has it, its comment and doc changes and history included, and nothing changes it. The session's own branch, scratch files,#codebaseimports and views stay as they are, and what's read is rendered as that branch has it, never from another branch's files (tcm_atwithbranchreads a line of that branch's view). (TCM_BRANCH=<b>, which a tcm run in a shell used for this, makes write commands act on that branch too.) A command that changes things, given--branch, is refused; - is cleaned up: when the server of a session on a branch made for it stops, the branch goes, with its
directory, if
mainhas everything on it (nothing was changed, or it landed) and no scratch file holds unsaved work. Otherwise it stays, andtcm statuslists it ("branches with work main doesn't have"), for you to land, for another session to land (tcm_mergewithbranch), or for an agent to go on with (a server started again from the same directory does;tcm_cli ["switch", "<it>"], ortcm mcp --branch <it>). A server that was killed, rather than stopped, can't clean up after itself: the next session's server, starting, takes the branches servers made for themselves (session/<date>-<id>) that no running server works on, on the same terms, and session directories whose branch is gone, unless a scratch file in one holds unsaved work (tcm statuslists those, to save from or delete).tcm branch <b> --deletetakes the branch's directory too, unless it holds unsaved work, and refuses while a session's server works on the branch. A server claims its branch, and one is cleared away, in a write transaction on the codebase, deciding by the servers' records (.tcm/mcp/<pid>.json): a server starting meanwhile can't lose its branch to another's cleaning up.
Test results (passes) are cached by hash, for everyone: a test one session ran doesn't run again in another for
the same code. Tests don't see the session's branch: a test that opens a codebase of its own finds it as it is. A session is also how to keep working while something long reads main (pnpm verify in tcm's own
repository): the session's changes don't touch it until they land.
Sessions side by side share one machine. Their test runs (tcm_test, a tcm_merge's, a verify's) take suites
from one budget, so however many agents test at once, no more suites run
than the machine has slots for. A run that waits says so, and on whom, once (and how long it took), not line after
line; what the tools report keeps one line of it, or the total in the summary (…, 6m12s waiting for slots).
In tcm's own repository, a session's verify (tcm_cli ["run", "tcmts.dev.verify"], on its branch) builds in a place of its own
(developing): verifies of different sessions never wait for each other;
only main's, making its tcm the known-good one, waits its turn.
33.4 Background jobs
A job (background: true) is a process of its own, started detached from the server, with its files in the
session's directory (.tcm/sessions/<branch>/jobs/; .tcm/jobs/ for a --direct server): <id>.json says what
it runs, when it started and its process, <id>.log has what it prints as it comes, and <id>.exit how it ended
(its exit code; a tool's job, what the tool answered). A tcm_merge or live job is the call asked of a server of
its own (tcm mcp), run by the job. So a job doesn't depend on the server that started it: when that server is
started again (a new tcm, or a crash), the job runs on, and the new server's tcm_job lists it ("started by an
earlier server"), follows it (wait), and says how it ended, all it printed included. A tcm_merge running when
its server is replaced still lands, or fails as it would have, and what it said is there to read. Ids go on across
servers: a session's job 3 is always the same job.
tcm_job with stop ends a job of the session: what it runs is stopped with it (its process group, asked, then
made to after five seconds), and it says what the job had printed; one stopped is listed so. A job whose process is
gone without saying how it ended (killed from outside, or with the machine) is said to be, with what it printed and
where its log is; an id the session never gave is said so, with the ids it has. A job's files are cleared away a
day after it ends (its id isn't given again), and go with the session's directory when the session's cleaned up.
33.5 A new tcm, without restarting
tcm mcp runs the server under a small supervisor. When tcm is replaced (upgraded, or, in tcm's own repository, a
new known-good build verified), the supervisor starts the server again at once, replays the client's handshake,
and tells the client its tools changed (notifications/tools/list_changed), so new tools and fixes reach a running
agent without restarting it. What the client asks from then on goes to the new server; the old one answers what it
was asked before (a request it never answers doesn't hold the new one back), then stops. Jobs it started run on,
and the new server follows them (background jobs).
A call with an option its tool doesn't have is refused rather than run without it, saying which, the build
answering (as tcm_status names it) and the options it has (tcm_cli has no `cwd` option in this build (#ycaei6cx): known are args, background, live), options of the objects in a call (each of patches) included. So a
call written for a newer tcm than the one serving it, before the supervisor has started that one, does nothing
rather than something else (a cwd ignored would run the command on this codebase). A live call is checked by the
codebase's own tcm, which answers it, so it takes the options that one has.
33.6 Without MCP
The CLI is the same workflow: tcm edit <name>, edit the scratch file, tcm check, tcm update, tcm todo,
tcm test. tcm help lists every command.