Skip to content

Conflicts as data

In Git, a conflict is a halt. The merge stops, your working tree fills with <<<<<<< markers, and you are stuck in a special, anxious mode until you fix it — you can't easily switch tasks, your teammates' merges hit the same wall, and the operation you ran reports failure.

TOVIO treats a conflict as data, not an event. When two changes can't be merged automatically, TOVIO stores the conflict as a first-class object and the merge completes. The conflict becomes an open question the repository holds for you — visible, tracked, resolvable later — instead of a wall you have to clear before doing anything else.

✓ landed `feature/cgm-sync` into `my-lane` with 3 conflict(s)  9f2c1a
    ⚠ content       src/integrations/cgm/dexcom.ts
    ⚠ content       src/integrations/cgm/types.ts
    ⚠ delete/modify tests/integrations/cgm.test.ts

  nothing is blocked — the conflicts are stored as data.
  resolve them:   tovio resolve
  list them:      tovio status --conflicts

Note the glyph: that is a success line, not an error. The land succeeded. There are open questions attached to its result.

flowchart LR
    M["tovio land / sync"] --> A{"auto-merges?"}
    A -- yes --> OK([✓ clean merge])
    A -- no --> S[["✓ merge completes<br/>+ typed conflict object stored"]]
    S --> W["Keep working:<br/>commit · switch · sync · stack"]
    W --> R["tovio resolve when ready"]
    R --> G[["Landing it on requires<br/>conflict-free tips<br/>(and the protected-lane gates)"]]
    G --> OK2([✓ done])

More diagrams: Conflict workflows.

What "conflict object" actually means

A conflict object holds both sides of the unresolved region, plus the merge base, as structured data inside the object graph. It is a normal versioned object — content-addressed, syncable, part of history. Because it's data:

  • The merge that produced it completed, so the operation didn't fail.
  • You can commit, switch, sync, and stack on top of a state that contains conflicts.
  • When someone resolves the conflict, the resolution propagates everywhere that same conflict appears, survives history rewriting (amend / rebase), and does not reappear — rather than being re-fought in every lane that inherited it.

Mental model

A conflict is like a type error, not a runtime crash. It doesn't stop you from editing other files, saving, and committing — it's right there in the output, tracked — but the affected file isn't "clean" until you resolve it. The compiler analogy is exact: a type error is information the system is holding for you, not a failure of the build system.

This rests on TOVIO's typed three-way merge over the commit DAG.

The shape of a conflict

A conflict object is file-granular: each one is anchored at a single path and carries that file's base / ours / theirs sides. Two files in conflict are two independent objects — there is no umbrella "conflict set" that bundles a merge's conflicts together. A merge or land simply completes and leaves however many single-file conflict objects it produced, each content-addressed on its own.

Underneath a single file, resolution is region-granular. A text conflict isn't one indivisible blob — it's a sequence of conflicted regions (hunks) with clean content between them, and each region is resolvable on its own. That's why a file can be partially resolved: some regions settled, others still open. Those regions are computed on demand from the three sides plus the two changes in conflict; they aren't stored as child objects, so every replica re-derives the identical breakdown instead of trusting a second copy.

And the whole thing is keyed on the change pair — the two changes that diverged, by their stable identity, ordered canonically so an A-into-B merge and the same conflict met later as B-into-A carry the same key. That key is what makes a conflict's identity independent of the operation that surfaced it: a land, a merge, a rebase, a pull, or a re-sync all produce the same conflict — which is exactly why a resolution recorded once (next section) re-applies everywhere it recurs, rather than being tied to the one operation that happened to expose it.

The model in one line

File-granular first-class objects, region-granular resolution computed underneath, with the whole thing keyed on the change pair — so a conflict's identity is stable and operation-independent by design.

"Non-blocking" has a precise scope — and hard limits

This is the part it's important to get right, because "non-blocking" is easy to over-read. A repository with open conflicts is always a consistent VCS state: you can commit, switch, sync, and stack on top of it. But it is not necessarily a buildable state — a conflicted source file holds both sides as data, so it won't compile and CI will fail on it. Keep three things separate:

Non-blocking — within your own stack and for independent work

A conflict in chg-003 never stops you from starting chg-004. A teammate's conflict never stops your unrelated work. Conflicts don't quarantine the repository; they sit where they are.

Strictly blocked — landing a change that still holds conflicts

tovio land and the Forge refuse to land when the source tip, the target tip, or the merge base still contains unresolved conflicts — resolve first. Onto a protected lane like main or release the land gates enforce conflict-freedom as an explicit gate on top of that. Non-blocking does not mean "ship broken code." Land is the build gate. A conflict may sit in your private work; it may not travel onward through a land.

Gated — when you pull someone else's conflicted change as a dependency

If a teammate's change that you build on top of still has unresolved conflicts, TOVIO will not silently materialize those conflict markers into your working tree and break your build. You wait for them to resolve — or you opt into tovio sync --isolate to build against the last-known-clean version of their change.

The boundary that matters

"Non-blocking" is about your stack — it does not propagate unresolved conflicts across developers' build trees.

A worked example makes the line concrete:

  • Alice lands a change that forces a conflict into Bob's unlanded chg-002. That conflict is Bob's to resolve. It doesn't block Alice — her land succeeded — and the conflict is now stored on chg-002.
  • Carol's UI work depends on chg-002. When Carol runs tovio sync, TOVIO does not drop Bob's conflict markers into her tree. It tells her chg-002 is conflicted and offers --isolate so she can keep building against the last clean version.

So the conflict is real, it's tracked, and it's assigned to exactly one person's stack — without spraying broken markers across everyone who happens to depend on the conflicted change. You can always check where you stand with tovio change health <id> and tovio status --conflicts.

Resolve once — and it stays resolved

Because a conflict is stored data with a stable identity, a resolution you record once is something TOVIO can carry forward. Three guarantees follow, and together they replace Git's rerere ("reuse recorded resolution") with something that just works by default:

  • Resolve once, applies everywhere. The same conflict appears with the same identity wherever it shows up. Resolve it in one place and TOVIO applies that resolution to every other context where the identical conflict recurs — instead of asking three developers to make the same edit three times.
  • Survives history rewriting. A resolution is keyed to the changes in conflict, and a change's identity persists through every rewrite. So a resolution recorded before you amend or rebase still applies after — those paths, and the descendant cascade a land triggers, run the replay. (change absorb still folds through the pure core path, so a resolution does not yet replay there; routing it is a tracked follow-on.)
  • Doesn't reappear. Once resolved, a conflict stays resolved across a re-sync or a rebase. It won't silently come back and make you re-fight it. (Changed your mind? The specified tovio conflicts reopen verb for deliberately reopening a resolution is not yet shipped.)

This is a direct consequence of conflicts being durable data rather than ephemeral working-tree state.

And you can record why you resolved it that way. A resolution captures what won; TOVIO can also capture the reasoning behind it. tovio resolve --why "<reason>" (with optional --rejected / --dead-end / --confidence) attaches a short, structured rationale to the resolving commit — the same signed "why" record TOVIO already keeps for agent commits, surfaced by tovio log --why — so the thinking that settled the conflict travels with the resolution instead of evaporating into a commit message. On a protected file that rationale is sealed to the file's readers, exactly like the resolved content, so the reasoning never leaks. It is descriptive only: it explains the choice, it never grants or gates anything.

Why, not just what

A resolution answers what won; --why answers what you weighed and why the other side lost. It reuses TOVIO's existing rationale record, so the reasoning is attested and — on protected paths — sealed, never a strippable side-note.

The sound merge engine has landed

The merge engine supports typed conflicts, whole-file and region rerere replay, and rewrite-aware resolution propagation — clear, region, and sealed protected-path caches replay and reseal across rebase and land cascades. Two follow-ons stay open: routing change absorb's cascade through the same replay pipeline, and protected sealed-cache hit/reseal integration evidence with an OS keychain on each desktop platform (a tracked Phase 5 item). The user-facing behavior remains the contract it implements.

A note on divergence

There's a second kind of "conflict" worth distinguishing. When two replicas advance the same lane to concurrent commits and then sync, TOVIO reconciles the two on the lane — it never picks a pointer winner and strands the loser, and it never asks you to force-push. It runs the same three-way merge as land: if the sides merge cleanly the lane advances to a two-parent merge commit; if they don't, it advances to a two-parent commit that carries conflict objects — the very objects described above. Both diverged tips become parents of the new lane tip, so nothing lands off-lane and no work is stranded. A content fork on sync is just an on-lane merge-or-conflict commit. The CRDT refs still run underneath, but only to propagate that reconciliation pointer between replicas. Both are covered on Offline & distributed.

Where to go next

Last reviewed September 9, 2026

Suggest an improvement to this page Not for security reports — see disclosure