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.
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 |