Collaboration & Sync
Peer synchronization, CRDT reconciliation, multi-repo federation, and distributed team workflows.
Overview
Trellis is built for distributed teams from the ground up. The sync layer implements a have→want→ops→ack protocol with support for both linear (sequential) and CRDT (concurrent) branch modes, over HTTP or WebSocket transports.
For ephemeral multiplayer UI (presence, chat, cursors) that does not write to the op log, see Realtime (ephemeral multiplayer).
For typed live reads on persisted entities (lists, relations, framework hooks), see Typed SDK.
For an interactive todo list + causal DAG view of the op stream, see Agentic state.
Sync CLI
trellis sync status # Local sync state
trellis sync reconcile --remote <path> # Reconcile with another local repo
How Sync Works
Every sync session follows a four-step handshake:
Engine A sends 'have' { heads: { main: 'h42' }, opCount: 42 }
→ Engine B compares to its own heads
→ B sends 'want' { afterHash: 'h38' }
→ A sends 'ops' [ op39, op40, op41, op42 ]
→ B integrates new ops
→ B sends 'ack' { integrated: ['h39', …] }
In linear mode, B simply filters out duplicates and appends. In CRDT mode, B runs the full reconciler to interleave ops by timestamp and detect conflicts.
CRDT Reconciler
The standalone reconciler works without a network connection useful for merging local branches or resolving import conflicts:
import { reconcile } from "trellis/sync";
const result = reconcile(localOps, remoteOps);
result.merged; // Op[] interleaved by timestamp
result.forkPoint; // string last common op hash
result.conflicts; // string[] files with divergent changes
SyncEngine
The full SyncEngine wires a transport to your op store and handles the protocol automatically:
import { MemoryTransport, SyncEngine } from "trellis/sync";
const transport = new MemoryTransport("peer-a", "Alice");
const engine = new SyncEngine({
localPeerId: "peer-a",
transport,
getLocalOps: () => ops,
onOpsReceived: (newOps) => {
/* integrate */
},
branchPolicy: { linear: false }, // CRDT mode
});
await engine.pushTo("peer-b");
await engine.pullFrom("peer-b");
engine.reconcileWith(remoteOps);
Transport Options
HTTP Transport
For async sync over a REST endpoint:
import { HttpSyncTransport } from "trellis/sync";
const transport = new HttpSyncTransport("peer-a");
transport.addPeer("peer-b", "http://192.168.1.10:4200");
WebSocket Transport
For real-time collaborative sessions:
import { WebSocketSyncTransport } from "trellis/sync";
const wsTransport = new WebSocketSyncTransport("peer-a");
await wsTransport.connect("peer-b", "ws://192.168.1.10:4201");
Memory Transport
For testing or in-process peer scenarios:
import { MemoryTransport } from "trellis/sync";
const transport = new MemoryTransport("peer-a", "Alice");
Multi-Repo Federation
Link separate repositories so knowledge and references can cross project boundaries:
import { formatCrossRepoRef, MultiRepoManager } from "trellis/sync";
const repoManager = new MultiRepoManager(kernel);
// Link a remote repository under an alias
await repoManager.linkRepo("backend", "/path/to/backend-api", "Backend service");
// Create a cross-repository relationship
await repoManager.addCrossRepoLink("proj:frontend", "dependsOn", "backend", "lib:api-client");
// Creates: proj:frontend --dependsOn--> @backend:lib:api-client
// Discover what references an entity in a linked repo
const refs = repoManager.findReferencesTo("backend", "lib:api-client");
Cross-repo references use a @alias:entity-id format. The formatCrossRepoRef helper constructs these:
const ref = formatCrossRepoRef("backend", "lib:api-client");
// → '@backend:lib:api-client'
Branch Policies
Branch policies govern how ops are integrated when syncing. Set them per-branch in .trellis/config.json:
{
"branchPolicies": {
"main": { "linear": true, "requireSigning": true },
"feature/*": { "linear": false }
}
}
| Policy | Description |
|---|---|
linear: true | Ops must arrive in order; duplicates are dropped |
linear: false | CRDT mode concurrent ops interleaved by timestamp |
requireSigning | Ops without a valid Ed25519 signature are rejected |