History, branches and merging
Names are the only thing in a codebase that changes, so history is a history of names:
- Each command that changes names makes one history entry. An entry is a state of all your names and the entries it came from.
- An entry's hash comes from just those, not from who made it, when, or its message. So the same change made from the same point is the same entry, with the same hash in every codebase that has it.
- A branch points at its latest entry. Every history starts from the same empty entry, so any two codebases share an ancestor.
- History is never rewritten, and code is never deleted: undoing is a new entry.
How code reads: comments, layout and docs
A definition's hash is its code. Its comments, layout and doc aren't in it (hashing). How a definition reads is still part of a branch, and changes with it as its names do:
- A change to only how a definition reads is this branch's. Saving the same code with other comments, layout,
names for what it imports from outside the codebase, or doc (
~ text,~ doc),tcm fmt, and a mirror spelling code as its files do all keep the hash. They change how the definition reads on this branch only: other branches read it as before. - It's a history entry like any.
tcm historylists it (reads differently),tcm diffshows it,tcm resetandtcm revertundo it, and push and pull carry it. A codebase receiving a text checks it's the same code. - Merges take it three-way, as they take names: a side's change to how a definition reads is kept.
- Both sides changed the same definition's comments or doc differently: this branch's is kept, and the merge says
so (
kept). - One side changed its comments or doc and the other its code: the comment change is merged into the new code as
text (
merged as text). Where that overlaps, would change the code, or the other side removed the definition, it'sdropped:tcm editit to write it again.
- Both sides changed the same definition's comments or doc differently: this branch's is kept, and the merge says
so (
Reviewing changes: tcm diff
tcm diff reviews changes by meaning:
tcm diff- what the last command changed
tcm diff <n|#hash>- changes since a history entry
tcm diff <branch>- changes against another branch
tcm diff <a> <b>- changes between two branches or entries
tcm diff <#def> <#def>- two definitions' code, line by line
tcm diff <conflicted name>- the merge base's definition against each side's
- Edits are line diffs, with each side's own names.
- Definitions re-hashed only because a dependency changed are listed apart, with what triggered them.
- Renames and aliases are shown as such.
- Renaming locals, reformatting or changing comments doesn't change the hash. Such a definition is listed apart
(
comments, layout or doc changed), with a line diff of how it reads.
History and undo
$ tcm history 3
#upoceguk 0 2026-10-29 01:08:47 mv geo.distance geo.dist
geo.dist + #ot2b562p
geo.distance - #ot2b562p
#eizpcqxs 1 2026-10-29 01:08:47 update geo.distance
geo.distance #c5ttv34t → #ot2b562p
and 1 updated because a dependency changed (`tcm history --all` lists them)
#ufvwor4x 2 2026-10-29 01:08:46 add geo.pathLength
geo.pathLength + #2gf3dz43
tcm history [n]shows this branch's latest entries, newest first: 30 unlessn(or-n <n>,--limit <n>) says. Each has its hash, how many entries back it is, its message if it has one, and the command that made it, named by what it changed (update lib.f, lib.g (+3), not the scratch file it came from). Under it is what it edited, added, removed and renamed, and a count of definitions only re-hashed because a dependency changed;--alllists those too. A merge is one entry, listing the history it merged.tcm reset <n|#hash>brings back the names as they were at an entry, as a new entry on top. A reset can be reset too, and it's safe after a push. In an app it's also how you roll back a deploy (production).tcm revert <n|#hash>undoes one entry (a landing, say) and nothing after it, as a new entry. What it changed goes back three-way, so changes made since stay; a change made since to what it changed is a conflict (settle it).- An entry is a number or a hash. A number counts back on this branch (
tcm reset 1undoes the last command). A hash can be from any branch's history. The#is optional, for shells that read#…as a comment (##abcis the same hash too).
Messages
An entry can say what it's for, as a commit message does. Pass -m <message> (or --message) to a command that
makes one: add, update, patch, mv, alias, rm, resolve, import, upgrade, fmt, merge, revert,
reset. Write a line saying what and why, then more after a blank line if needed:
$ tcm patch geo.dist --old 'Math.abs(a - b)' --new 'Math.hypot(a, b)' -m 'geo.dist: Euclidean, so 2-D points work
Callers pass x and y offsets now.'
$ tcm history 1
#izftjcue 0 2026-11-03 05:00:06 geo.dist: Euclidean, so 2-D points work
Callers pass x and y offsets now.
update geo.dist
geo.dist #6udjjymz → #mxhghddi
- It's the entry's title.
tcm history,tcm reset's list of what to undo and the web UI call an entry by its message's first line, with the rest and the command under it. - It's in no hash, so you can change it without changing history.
tcm describe [n|#hash] -m <message>gives an entry a message or replaces it;-m ""takes it away;tcm describe [n|#hash]shows it. The default entry is the latest. A command with-mthat makes an entry that already exists (a one-step branch landed squashed with nothing new onmain) gives that entry the message. - It goes where the entry goes.
tcm push,tcm pullandtcm migratecarry it. A tcm from before messages shows it under the entry's command. A codebase that already has the entry keeps the message it has: one changed withtcm describeafter a push changes here, not on the remote. - A landing says what the work was for.
tcm merge <branch> --squash -m …makes one entry of a branch's work, titled by its message; an agent'stcm_mergeasks for one (agents). A merged (not fast-forwarded) contribution with a description has its title and description as its message.
Branches
A branch is a set of names, the history that led to them, and how each definition reads. Definitions themselves are shared between branches.
tcm branchlists branches;tcm branch <name>creates one from this branch.tcm branch <name> --deletedeletes one: its names, and its session's directory unless that holds unsaved work. The history it made stays. It refusesmain, the branch you're on, and a running session's branch.tcm switch <name>changes branch. In a session,TCM_BRANCHkeeps a process on its own branch instead.
Merging
tcm merge <branch> goes by history:
- If this branch already has the other's history, there's nothing to merge.
- If the other branch has this one's, this branch moves to it: a fast-forward, with no merge entry.
--no-fast-forwardmakes a merge entry anyway, so what came in is one entry thattcm revertundoes whole. - Otherwise it's a three-way merge of names from the best common ancestor, recorded as an entry with both as parents. Later merges start from there, in any codebase with this history, so pulling and pushing back and forth never merges the same change twice.
--squashmakes what came in one entry whose only parent is this branch's head (even where it could fast-forward). The other branch keeps its own entries, and pushing this branch sends only what it names, not every version along the way. Once its names are here, the other branch's work counts as merged:tcm statusdoesn't list it, and squashing it again brings nothing. Sessions land this way (agents).- A branch that made releases keeps its history,
--squashor not: still one entry, with the branch's head as a second parent, andtcm mergenames the releases. Otherwise the next release here would plan the first again, and a push wouldn't carry the releases.
How a merge decides
A name changed on one side takes that side.
A definition only re-hashed because a dependency moved doesn't count as a change.
A definition both sides edited is merged as text: its code as
tcm viewshows it (a page: its Markdown), three-way from the base's, asgit merge-filedoes. If the edits are apart (different methods of a class, different bullets of a page) and the result typechecks with the merge's other changes in, it's taken (merged as text). Otherwise the name is a conflict, listed by the merge and bytcm todo, each side labelled (ours: this branch, theirs: the one merged in):conflict lib.Store: #g3o6xnsj (ours: main) vs #5ren724d (theirs: b) tcm edit lib.Store merges them (conflict markers where they overlap); tcm resolve lib.Store --ours or --theirs keeps one
A name one side deleted is kept if the other side's code still uses it.
Everything is re-derived against what the merge names, in dependency order, as an update re-derives its dependents. Code taken from one side follows the other side's changes to what it uses. What no longer typechecks goes to
tcm todo, and the merge names it.A conflicted name holds both definitions, each re-derived against the merge. Code using one directly keeps using it. Code reaching it only through definitions both sides re-derived uses this branch's, so the other side's code may not typecheck until the conflict is settled. A definition that can't follow a change (it doesn't typecheck against it) keeps what it used:
tcm view '#<hash>'names that by its name elsewhere (an older lib.save,lib.save on main).A conflict's sides and merge base are kept with the branch until it's resolved, and travel with push and pull.
A merge left to settle waits apart
A merge that leaves anything to settle (a conflict, or code that stops typechecking) isn't made on the branch. The
branch stays where it was, and the merge waits apart: tcm todo and tcm status say so first (merging main into s: not finished).
- It writes scratch files holding what's to settle: each conflicted name's sides merged, as
tcm editwrites them (lib.k.ts), and what stopped typechecking inmerge-fix.ts. Only what failed itself is there; what failed because something it uses did follows once that's fixed. - Commands on the branch (
tcm edit,update,resolve,todo,view,test, …) work on the merge as merged. - Once nothing's left to settle, the branch moves to the merge, with its entry and every one that settled it.
Until then the branch is as it was, for every other process too, and
tcm pushsends it as it is. tcm merge --abortdrops the merge. Merging again while one waits is refused.tcm pullmerges the same way.
Landing
On main (or with --squash, as sessions land), tcm merge <branch> lands the branch:
mainis merged into the branch first, there. If that leaves something to settle, it waits on the branch (above) and nothing lands. What both sides changed and merged cleanly as text is named (both changed lib.x): read it before it lands, as tests can't tell what each side meant.- Then the branch's
tcm todoand the tests the change reaches (tcm test) run on the branch. Anything left to fix, or a failing test, and nothing lands. - Then the branch lands: a fast-forward, or one entry with
--squash. Ifmainmoved meanwhile and it's no longer clean, it isn't made. - Squashed,
mainis merged into the branch once more (changing none of its names), so work kept on the branch merges cleanly with its own landed version.
So main never holds a conflict, or code that doesn't typecheck, from a merge. A remote's branch (tcm merge origin/main) isn't landed: it's merged, and only if that's clean. An agent's tcm_merge is this landing.
Settling a conflict
Merge it by hand: tcm edit <name>. It writes a scratch file named after the name (lib.k.ts; a page: its
Markdown file) holding both sides merged three-way from the base, as tcm view shows each. Where both changed the
same lines, the file has git's conflict markers, each on a line of its own:
<<<<<<< ours (session/x) const a = 10; ||||||| base const a = 1; ======= const a = 100; >>>>>>> theirs (main)
It says what each side changed, how many stretches both changed, and, where nothing's marked, whether the merge typechecks:
wrote lib.k to lib.k.ts
lib.k: ours (s) and theirs (main), merged from the base
ours: +1 −1 lines, at lines 3–9
theirs: +2 −2 lines, at lines 3–9, 11
both: 1 stretch changed differently, marked at lines 3–9
Keep what each side should, take the markers out, and tcm update lib.k.ts. That resolves the conflict and
re-derives what used either side against it.
- Any definition saved at a conflicted name resolves it, from
tcm editor written by hand:tcm checksaysresolves, and history names the entryresolve lib.k (merged)(or(ours),(theirs)where it's that side's). tcm checkandtcm updaterefuse a file with markers left in: lines starting<<<<<<<,=======and>>>>>>>, in order. Text only mentioning them is fine.- Each conflict gets its own file. An existing file is written over only if nothing unsaved would be lost;
otherwise
tcm editsays what's in it that isn't saved. - A conflict that uses another conflicted name can't typecheck until that one's settled (
uses lib.k, which is still conflicted). Settle that first, or edit both together,tcm edit lib.m lib.k: one file, saved as one, resolves both.
Keep one side: tcm resolve <name> --ours (or --theirs), or tcm resolve <name> <#hash> for that
definition. Everything that used the others is re-derived against it, and history names it resolve <name> (ours)
(or (theirs), or the hash). Resolving one conflict re-derives code that used what it dropped, so a hash copied
before can be stale: prefer --ours/--theirs.
tcm diff <name> shows the base's definition against each side's.
Criss-cross merges
Branches that merged each other (two sessions landing on main and merging it back in, say) can have several best
common ancestors. As git's recursive strategy does, tcm merges those first, oldest first, into a virtual base, then
merges the two branches from it, so neither side's undoing of the other's changes is lost. A conflict while
building the base stays a conflict in it: where the two branches settled it alike the merge agrees, and where they
settled it differently it's a conflict again. The virtual base is never saved.
tcm todo
tcm todo lists what's left to fix: a merge waiting apart first, then conflicts (each side labelled, with how to
settle it), then dependents that stopped typechecking after an update or merge. tcm edit them, fix, and
tcm update.
- It shows the first 20 dependents (fixing those often fixes the rest);
--allshows every one. --jsonprints all of it as JSON for tools: the branch, a merge not finished, conflicts with their sides, what needs fixing and why, and code using unnamed versions.- It's worked out from what the branch names now. Going back (
tcm reset, a merge dropped) leaves nothing to fix.