Skip to main content
There is a brain on disk and a brain in a registry, and they are not the same. That sentence covers four different situations, and three of them have a one-command answer that is safe. The fourth is a reconciliation. The mistake this page exists to prevent is treating all four as the fourth — or, worse, treating the fourth as one of the first three and losing a week of somebody’s work. If you are here every day because several people publish to one tag, the fix is not on this page: give each of them a branch. Establish which situation you are in before running anything that writes. Every command in the first section moves no pointer and discards nothing.

First, find out what is true

Read three things:
  • data.origin in brain state — reference and tag are the copy this brain was installed from. null means it was never pulled: it was born here, and “remote” is only whatever vitruvio.toml or the logged-in account derives. data.ancestry lists the snapshots this brain has been, most recent first.
  • data.tags — whether the tag you are thinking of exists at all. A tag nobody pushed yet is not a divergence, it is a first push.
  • data.is_noop and data.impact in plan-pull. is_noop: true means the registry holds exactly what is installed. impact.blocks is how many blocks were committed here since the last pull — read impact.certainty first, because a plan can be approximate, and unknown comes with blocks: null.
When both sides have moved, bring their history alongside yours without adopting it and look at the split:
tree reports ours, theirs, the ancestor they parted at, and per module how many blocks were added_by_us, added_by_them and removed. That table is the whole picture.

The four situations

Behind is the only case where pull is right, and it is right because impact says there is nothing to lose. If impact.blocks is greater than zero, or certainty is unknown, you are not behind — you are diverged and have not fetched yet. After every successful dist pull, run brain verify and auth status on that same brain. If auth status reports a trust root, the installed brain is governed: run auth attribution too, and treat a head that is not authorized, or an audit that is not complete and fully_vouched, as a possible authorship breach to review before calling the brain authenticated. It is not proof of corruption — a merge may legitimately introduce an actor the head’s signer does not vouch for — but it is not something to wave through either. Ahead needs no ceremony. A push that is a fast-forward is refused by nothing. If it comes back 8, the registry moved between your plan-pull and your push; that is the fourth row now, not a retry.

Diverged: keep both sides

Step 1 does one of four things, and data.reconciliation.why says which: THEIRS is data.reconciliation.theirs, which is also data.digest. The fetch does not remember it for you.

No strategy declared

The three strategies land byte-for-byte the same composition and differ only in whose name stays on the incoming work — merge keeps their snapshots and signatures, rebase and squash reissue the work under yours. That is an attribution decision, and vitruvio refuses to make it for you:
merge is the only strategy under which a contributor’s snapshots still cover something; rebase invalidates any root they already published. Do not add reconcile = ... to vitruvio.toml just to make the fetch go through; if you want a standing choice for next time, declare the one you actually mean.

Not clean

The fetch reported and stopped, because opening a reconciliation blocks every write until it is resolved. Open it deliberately, with the strategy you chose:
Exit 12 with halted: true is a question, not a failure. What each verdict means and which decision it admits is the subject of Joining two histories. Two rules matter here: a rejected block is never admitted, and if withdrawn names blocks, they are your work leaving — look at how many, and from which modules, before accepting. vitruvio reconcile abort abandons an open reconciliation and writes nothing. It always works.

What never fixes a divergence

  • dist push --force. It overwrites the registry with your history and discards theirs. There is no undo and no command that recovers what a force-push replaced. The flag exists for a version that is genuinely to be thrown away, and “I want the push to go through” is not that.
  • dist pull to escape exit 8. A pull adopts the published composition; every block committed here since the last pull leaves it. The blobs remain and the old snapshot is retained, but no command restores it. Pull is the behind answer and only the behind answer.
  • Retrying the push. Exit 8 is not a transient failure and comes back identical every time. The registry did not hiccup; it refused.
  • Editing vitruvio.toml to declare a strategy, flip publish = false, or point at a different reference so the refusal goes away. Each of those is a statement you make, not a workaround.

Things that look like a divergence and are not

In a project with several brains

Each named brain has its own remote and its own answer. Run vitruvio project show and go through them one at a time with --brain NAME; a brain marked publish = false can only ever be behind, and one with nothing committed is skipped by dist push --all rather than failed. A project is not diverged because one brain is: name the brain.