The state left the repo
For most of this project's life a bridged repository had a .bridge/ directory in it. In 0.13.0 it leaves. The move is one line in a changelog; getting there is fifteen commits, almost none about copying files.
For most of this project's life, a bridged repository had a .bridge/ directory in it. Session references, watermarks, pending markers, checkpoints. It was gitignored automatically, it was never transcripts, and I have written more than once on this site about why keeping it small and local was the right call.
In 0.13.0 it leaves. Runtime state now lives in a machine-local store, and the project directory gets nothing at all.
The move is one line in a changelog. Getting there is about fifteen commits, almost none of which are about copying files.
Where it went
macOS ~/Library/Application Support/context-bridge
others $XDG_STATE_HOME/context-bridge
(default ~/.local/state/context-bridge)
override CONTEXT_BRIDGE_HOME
Windows included. Projects are keyed by a UUID in a registry, and the registry is resolved through the directory's filesystem identity — which is the subject of its own post, and which is why that piece had to be solid before this one could exist.
What the move buys
Three things that were awkward or impossible before, and which are visible in the new commands rather than in any argument:
A project can have more than one checkout. Lanes can now create or attach a Git worktree, so one conversation lives in one working tree and another lives in a second. State that lives inside a checkout cannot describe a project that spans several.
A project can move. bridge project adopt <id> reconnects a directory that changed filesystem identity to its existing store. The conversation history survives the move because it was never in the directory.
Git is no longer required. Ordinary projects do not need it installed. Worktree operations still do, because those genuinely are Git.
And one small piece of tidying: bridge storage cleanup-ignore exists to take the now-pointless .bridge/ rule back out of your .gitignore, previewing first.
Copying is easy. Retiring the original is not.
Every hard commit in this group is about the same moment: the new copy exists, is verified, and now the old one has to stop being authoritative. Getting the order wrong there is how people lose things.
fix: flush migration evidence before retiring source files is that arrow. If the receipt describing what was migrated is still in a write buffer when the originals are retired, a crash leaves a machine that has lost the source and cannot prove what replaced it.
When the flush itself fails, the code does not push on. It reports BRIDGE_MIGRATION_SYNC_FAILED and preserves both the published copies and the retired originals — an outcome that is messy on disk and honest about it, which is the right trade in that direction.
The filesystem boundary is where it gets interesting
rename() is atomic within a filesystem and unavailable across one. If your project sits on a different filesystem from your home directory — an external disk, a mounted volume, a container layer — then "move the old state aside" cannot be a rename. The usual substitute is copy-then-delete, which is two operations with a gap in the middle where the file exists twice, and then a moment where it exists zero times if the delete half-succeeds.
So the bridge does not substitute. It asks you for somewhere on the project's own filesystem to put the retired originals:
bridge storage migrate --retirement-dir /absolute/path/to/vault
The new runtime and its verified backup still go to the normal home. Only the original inodes stay in the vault, on the filesystem they were already on, moved by a rename that is allowed to be atomic. Without a suitable vault, cleanup refuses and preserves the source rather than performing a copy-and-delete it cannot make safe.
Two details in there that I think are the mark of someone who has actually been burned:
- a pending migration records the vault it chose and resumes there without the flag — but cannot change location after any source file has been retired, because a half-retired migration pointing at two vaults is unrecoverable by definition;
- and
storage plan --jsonreports the recorded recovery path, so the answer to where did my originals go is a command rather than a memory.
It refuses more than it performs
Reading the migration commits in order, the ratio is striking. Most of them add a refusal.
refuse migration when legacy launcher ownership is uncertain— if the bridge cannot establish that nothing is currently running against the old state, it will not migrate. A live launcher writing into storage that is being retired is the one scenario where careful ordering saves nothing.refuse unsafe migration journals and receipt reads— the evidence trail is itself read with the owned-file checks applied everywhere else in this release. A journal you cannot trust is worse than no journal, because you will act on it.allow explicit legacy identity confirmation before migration— an older registration is markedunverifiedand requires you to confirm it by hand. The bridge will not decide on its own that this directory is that project.- Unrecognised files are preserved, never deleted. If something is in the old store that this version does not recognise, it stays.
The cleanup of abandoned staging copies has the strictest rule of the set: they are removed only when their owner is no longer alive and their bytes exactly match the verified backup. Anything changed, unknown, symlinked, or owned by a live process is retained — and storage plan keeps listing it, with the reason, even after the migration has finished. A stale file with an explanation attached beats a tidy directory.
Preview is the default everywhere now
This release quietly adopted a convention across its whole new surface, and migration is where it shows best:
bridge storage plan read-only, changes nothing
bridge storage migrate performs it
bridge storage cleanup-ignore previews; --apply removes
bridge project retire <id> previews; --apply archives
bridge project purge <id> previews; --apply --confirm <id> deletes
purge needs --apply and the id typed back. The commands that cannot be undone are the ones that ask twice.
The lifecycle that came with it
Once state is machine-local rather than project-local, it outlives the directory, and something has to manage that. Hence bridge project list / adopt / inspect / recover / retire / restore / purge.
inspect is the one worth naming: it reads a retained store by UUID, without mutation, including for projects whose directory is gone. State that survives its directory needs a way to be looked at that does not require standing in it.
Retirement itself journals its intent — the registry records retiring before the rename, and on POSIX the parent directories are synced before the registry update lands — so an interruption leaves a state that can be read and resumed rather than guessed at.
What it does not claim
From the architecture doc, unprompted:
no evidence of power-cut recovery on every filesystem or storage controller
Locks, fsyncs, ordered writes and journals give you a system that survives process death and most interruptions. They do not give you a guarantee against a controller that lies about flushing. The documentation says so rather than letting the word "durable" carry an implication it has not earned.
That is the same instinct as the rest of this release: state what was verified, name what was not, and refuse where you cannot tell the difference.