AI agents — tcm-ts

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 main doesn'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 work main doesn'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's lines counts; the text as written, or a regular expression written /…/, as tcm_view's grep takes it (regex is the old way); under a 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 (lines or grep for only some of a large one, numbered: grep is text as written, a|b included, or a regular expression written /…/; lines as 120, 120-180, 120- or 1-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 (names both, one file). Saved with tcm_update (that file) once the markers are out, it resolves the conflict, named resolve <name> (merged) in history (merging)
tcm_patch
a small change in one step: find-and-replace on a definition's code as tcm_view shows it, checked and saved as tcm_update does, 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 as tcm_view numbers it, the edits made. Imports follow the code; one that can't be found that way (an alias) is given in imports ([{"name": "isImportDeclaration", "from": "typescript", "as": "ts_isImportDeclaration"}]), and goes if the code doesn't use it. patches changes several definitions, and pages, saved together as one update and one history entry, each edit matched against its definition as tcm_view shows it (each patch with imports of its own). Inside a template literal, old and new are as the source writes them, escapes and all (in JSON, "\\n" for the source's \n): an old found 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, new written alike, and said; a new that 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 (by tcm_check and tcm_update too)
tcm_add
code given in the call saved without a scratch file, into a namespace (update to 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 (keep to keep it). into for a file kept as written, as tcm_edit says. 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: side ours or theirs, as tcm_todo labels them (found as they are now, after what re-derived them since), or a hash
tcm_test, tcm_run
tests (cached by hash, names for only some, fresh to run them all) and suites (cached by the code they ran, definitions to leave them out, suites for only them; bin runs 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 n entries, 10 unless said; each named by what it changed, and listing it by name, the definitions only re-hashed because a dependency changed counted unless all)
tcm_merge
in a session, only when you've said to (approved; branch for an earlier session's): land its branch on main, once the tests the change reaches pass, as one entry called by its message (required: what the branch did and why, as main's history will say it); it says what landed by name and count (tcm diff has the code), as it does what merging main in did when that stops on a conflict (the conflicts, labelled by side, and what no longer typechecks), and of failing tests only the failures. background runs 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; wait waits for it to end, up to that many seconds; stop stops 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); live runs the codebase's own tcm, for a codebase that is one (tcm's); cwd runs it in another directory, on the codebase there (tcm -C); build runs 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_BRANCH names it, for the CLI too), and tcm switch there moves the session, not the codebase (below). That includes a change to only a definition's comments or doc: main reads it as before until it lands, and it's an entry in the session's history that tcm reset and tcm revert undo (history);
  • has its own scratch files, #codebase imports and definition views (src/, which its stack traces point into; main's .tcm/src is 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/, as tcm_status and 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_merge is tcm merge --squash on main (landing): it merges main into 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, then tcm_merge again) and runs the tests the change reaches; if they pass, it lands the branch on main as one history entry, squashed: its only parent is main's, so the session's own entries (each step it took) stay on its branch, and pushing main sends 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 revert undoes 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> --squash on main;
  • says what it has that main doesn't: tcm status in a session counts its branch's history entries main doesn't have and the names they changed, or says there's nothing, and that it lands with tcm_merge when 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 main doesn'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, and tcm_job follows 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 from main rather 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, which tcm_status lists), if no other session's server is on it. Not onto main: work lands there with tcm_merge. The branch it leaves, if a server made it, goes if main has all of it, as when a session stops;
  • reads other branches without moving: the tools that only read take branch (tcm_view with "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, #codebase imports and views stay as they are, and what's read is rendered as that branch has it, never from another branch's files (tcm_at with branch reads 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 main has everything on it (nothing was changed, or it landed) and no scratch file holds unsaved work. Otherwise it stays, and tcm status lists it ("branches with work main doesn't have"), for you to land, for another session to land (tcm_merge with branch), or for an agent to go on with (a server started again from the same directory does; tcm_cli ["switch", "<it>"], or tcm 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 status lists those, to save from or delete). tcm branch <b> --delete takes 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.

built from c482a14e