trellis/vcs
Version control model operations, branches, milestones, checkpoints, issues, diff, merge, and blob storage.
Overview
The trellis/vcs subpath exposes the operation types and domain-level building blocks for version control. Use it when you want Trellis VCS capabilities without the full platform surface.
import { BranchEntity, MilestoneEntity, VcsOp, VcsOpKind } from "trellis/vcs";
VcsOp
Every change is an immutable, content-addressed operation:
interface VcsOp {
hash: string; // 'trellis:op:<sha256>' content address
kind: VcsOpKind; // Operation type
timestamp: string; // ISO 8601
agentId: string; // Author identity (DID)
previousHash?: string; // Causal chain link
vcs?: VcsPayload; // Kind-specific payload (file paths, issue fields, store facts, …)
facts?: Fact[]; // Optional top-level facts (some op kinds)
links?: Link[]; // Optional top-level links (some op kinds)
signature?: string; // Ed25519 signature (when signing enabled)
}
Local durability and concurrent writers
When multiple Trellis processes write to the same repository path, the local VCS storage layer uses file locks to avoid dropped operations and duplicate issue IDs.
ops.jsonappends are lock-guarded and re-read from disk before write.- Writes use a temporary file plus atomic rename to avoid partial writes.
- Replayed duplicate op hashes are ignored as an idempotency guard.
- Issue IDs (
TRL-<n>) are allocated under a lock so concurrentissue createcalls stay monotonic.
If Trellis cannot acquire a local lock within a short timeout, the command fails with a lock-timeout error instead of risking log corruption.
Agent Lanes
Multi-agent work needs isolated causal journals per agent. Agent Lanes add per-agent op logs under .trellis/lanes/lane-{uuid}/ops.json, forked from an integration branch head and promoted explicitly into main.
Optional git worktree bind (W5, 3.2.3+): when lanes.worktreeBind is true in .trellis/config.json, createLane provisions a git worktree at .trellis/worktrees/<shortId>/. 4.0.0+ (ADR 0038): enterLane loads lane bytes from the lane git branch (auto-commit on exit); the op-log does not materialize file state. Pre-4.0: enterLane replayed lane blobs to the worktree. Without worktree bind, lanes still isolate op journals while agents may share the repo root on disk.
| Module | Role |
|---|---|
JsonOpLog | Integration journal (.trellis/ops.json) |
LaneOpLog | Per-lane journal |
lane.ts | LaneMeta, paths, createLaneMeta |
lane-promote.ts | Promote planning, conflict detection |
lane-materialize.ts | Integration cache + lazy lane overlay (W4) |
lane-worktree.ts | Git worktree provision / cleanup (W5) |
lane-disk-materialize.ts | Blob replay to bound worktree on enter |
import { createLaneMeta, JsonOpLog, laneDir, LaneOpLog } from "trellis/vcs";
Engine API (via TrellisVcsEngine): createLane, enterLane, leaveLane, dropLane, promoteLane, getMaterializationStats. Config: lanes.worktreeBind?: boolean.
Decisions are recorded in kernel/docs/adr/ (0001–0008, 0014, 0021–0022, 0038). Lane fork semantics: ADR 0006–0007. EAV store materialization: ADR 0008. Git byte authority: ADR 0038 (4.0.0+). 3.4.0+: VcsOp.laneId is envelope data (outside the hashed payload); read op.laneId, not op.vcs.laneId. CLI: trellis lane … (3.1.32+), worktree bind (3.2.3+), lane split (3.4.0+).
Operation Tiers
| Tier | Operations | Description |
|---|---|---|
| 1 | dirAdd, dirDelete, branchCreate, milestoneCreate, … | Structural VCS control ops |
| 2 | fileAdd, fileModify, fileDelete, fileRename | File lifecycle ops |
| 3 | astPatch | Semantic AST-level patches |
| 4 | Signatures, governance | Cryptographic layer |
EAV store operations
CMS and knowledge-graph entities persist as four dedicated op kinds in the integration journal. On materialization, decompose() passes vcs.facts and vcs.links through to the in-memory EAV store (kernel ADR 0008):
| Op kind | VcsPayload fields | Effect |
|---|---|---|
vcs:storeAssert | facts[] | Add facts |
vcs:storeRetract | facts[] | Remove facts |
vcs:storeLink | links[] | Add links |
vcs:storeUnlink | links[] | Remove links |
Entity ids use typed prefixes (person:…, organization:…, …), not a generic entity: prefix.
Branch Entities
Branches are graph entities with a pointer to their current head op:
interface BranchEntity {
id: string; // 'branch:<name>'
name: string;
headHash: string; // Current tip op hash
createdAt: string;
policy?: BranchPolicy;
}
// List branches
const branches = await engine.listBranches();
// Create a branch
await engine.createBranch("feature/auth");
// Switch branch
await engine.switchBranch("feature/auth");
// Delete a branch
await engine.deleteBranch("feature/old");
Milestone Entities
Milestones are human-curated markers over a range of ops:
interface MilestoneEntity {
id: string; // 'milestone:<hash>'
message: string;
fromOpHash: string;
toOpHash: string;
affectedFiles: string[];
authorId: string;
createdAt: string;
}
// Create a milestone
await engine.createMilestone({
message: "Add user authentication",
// fromOpHash defaults to last milestone's toOpHash + 1
// toOpHash defaults to current HEAD
});
// List milestones
const milestones = await engine.listMilestones();
Diff & Merge
File-level Diff
import { diff } from "trellis/vcs";
const result = diff(fromOpHash, toOpHash, { engine });
// { file: string; added: number; removed: number; hunks: DiffHunk[] }[]
Semantic Diff
import { semanticDiff } from "trellis/vcs";
const patches = semanticDiff(oldSource, newSource, "auth.ts");
// SemanticPatch[] symbolAdd | symbolRemove | symbolModify | symbolRename | …
Three-way Merge
import { merge } from "trellis/vcs";
const result = await merge("feature/auth", { engine, dryRun: false });
result.conflicts; // MergeConflict[] entity-level, with suggestions
result.clean; // true if no conflicts
Checkpoints
Auto-generated stable-state markers (created automatically when op count crosses a threshold):
interface CheckpointEntity {
id: string;
opHash: string; // Op at checkpoint time
snapshotId: string; // SQLite snapshot reference
createdAt: string;
}
Issue Tracking
Issues are first-class VCS entities, not an external system:
import { IssueEntity, IssueStatus } from "trellis/vcs";
interface IssueEntity {
id: string; // 'issue:TRL-<n>'
title: string;
description?: string;
status: IssueStatus; // 'backlog' | 'queued' | 'in_progress' | 'done' | 'closed'
assignee?: string;
labels: string[];
acceptanceCriteria: string[];
parentId?: string; // Epic via childOf link (set at create or update)
branchId?: string; // Auto-created branch when started
createdAt: string;
updatedAt: string;
}
Parent links are stored as childOf edges in the graph. createIssue accepts parentId; updateIssue accepts parentId (re-parent) or parentId: null (clear). Re-parenting emits vcs:issueUpdate with oldParentIssueId / parentIssueId so the prior link is removed before the new one is added.
See the Issue Tracking guide for the full lifecycle.
Blob Storage
Large file content is stored in the blob store and referenced by hash keeping the op log lean:
import { BlobStore } from "trellis/vcs";
const store = new BlobStore({ dbPath: ".trellis/blobs.db" });
const hash = await store.put(Buffer.from(fileContent));
const content = await store.get(hash);