trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
661 lines • 26.5 kB
TypeScript
/**
* TrellisVCS Engine
*
* The composition root that ties together the trellis-core kernel,
* the file watcher, the ingestion pipeline, and VCS middleware.
*
* Usage:
* const engine = new TrellisVcsEngine({ rootPath: '/path/to/repo' });
* await engine.init(); // scan + create initial ops
* engine.watch(); // start continuous monitoring
* engine.stop(); // stop watcher
*/
import { EAVStore } from './core/store/eav-store.js';
import type { IdentityResolver } from './identity/signing-middleware.js';
import type { OpProvenance } from './core/persist/canonical-op.js';
import type { VcsOp, TrellisVcsConfig } from './vcs/types.js';
import { BlobStore } from './vcs/blob-store.js';
import { BlobResolver } from './vcs/blob-resolver.js';
import type { EngineContext } from './vcs/engine-context.js';
import * as branchMod from './vcs/branch.js';
import * as milestoneMod from './vcs/milestone.js';
import * as checkpointMod from './vcs/checkpoint.js';
import type { ReentryCheckpoint } from './protocol/whereami.js';
import * as diffMod from './vcs/diff.js';
import * as mergeMod from './vcs/merge.js';
import * as issueMod from './vcs/issue.js';
import * as testRunnerMod from './vcs/test-runner.js';
import * as storeMod from './vcs/store.js';
import type { Atom } from './core/store/eav-store.js';
import type { EntityRecord } from './core/kernel/trellis-kernel.js';
import * as decisionMod from './decisions/index.js';
import * as transcriptMod from './vcs/transcript.js';
import { IdeaGarden } from './garden/index.js';
import type { ParseResult, SemanticPatch } from './semantic/types.js';
import type { ProjectContext } from './scaffold/infer.js';
import type { OpLog } from './vcs/op-log.js';
import * as laneMod from './vcs/lane.js';
import type { LaneMeta } from './vcs/lane.js';
import * as lanePromoteMod from './vcs/lane-promote.js';
import type { LanePromoteResult } from './vcs/lane-promote.js';
import type { MaterializationStats } from './vcs/lane-materialize.js';
import * as laneCoherenceMod from './vcs/lane-coherence.js';
import { type GitSyncResult } from './git/git-sync.js';
export interface InitProgress {
phase: 'discovering' | 'hashing' | 'recording' | 'scaffolding' | 'done';
current: number;
total: number;
message: string;
}
export interface InitRepoOptions {
onProgress?: (progress: InitProgress) => void;
indexWorkspace?: boolean;
}
export interface InitRepoResult {
opsCreated: number;
filesIndexed: number;
indexWorkspace: boolean;
context: ProjectContext;
}
export interface IndexWorkspaceResult {
opsCreated: number;
filesIndexed: number;
}
export type IntegrateOpRejectReason = 'invalid-kind' | 'hash-mismatch' | 'unauthorized' | 'missing-dependency' | 'apply-failed';
export interface IntegrateOpRejection {
op: VcsOp;
reason: IntegrateOpRejectReason;
message: string;
}
export interface IntegrateOpsResult {
applied: number;
skipped: number;
rejected: IntegrateOpRejection[];
}
export declare class TrellisVcsEngine {
private config;
/** Optional identity resolver for ingest-time signature verification (ADR 0022 Phase 3). */
private identityResolver?;
/** Local signing material; when present, the engine mints signed auth ops. */
private signingMaterial?;
private store;
private opLog;
private watcher;
private ingestion;
private agentId;
/** ADR 0021 §2 — stamped onto every op this engine mints. */
private provenance;
private currentBranch;
private checkpointOpCount;
private checkpointThreshold;
private _pendingAutoCheckpoint;
private _blobStore;
private _blobResolver;
private activeLaneId?;
private activeLaneLog;
private integrationCache;
private materializationStats;
private watchReconcileOnRestart;
constructor(opts: {
rootPath: string;
agentId?: string;
/**
* Optional custom op-log backend. Defaults to a filesystem-backed
* {@link JsonOpLog} at `<rootPath>/.trellis/ops.json`. Browser hosts
* can inject an {@link IdbOpLog} or other {@link OpLog} implementation.
*
* Callers that inject a non-filesystem backend are responsible for
* awaiting `opLog.load()` before passing it in if their backend's
* load is asynchronous.
*/
opLog?: OpLog;
/**
* Provenance stamped onto every op this engine mints (ADR 0021 §2).
* Set per construction site — the engine is built by the CLI, the MCP
* server and the UI server, each of which knows its own surface.
* Defaults to the honest `{ actorType: 'machine', origin: 'sdk' }`.
*/
provenance?: OpProvenance;
/**
* Optional identity resolver used to cryptographically verify op
* signatures at the ingest boundary (ADR 0022 Phase 3). When present,
* authorization-bearing ops (grant/zone) must carry a valid signature;
* when absent, the kernel still requires a signature envelope to exist
* (deny-by-default for unattributable auth ops).
*/
identityResolver?: IdentityResolver;
/**
* Local signing material (ADR 0022 Phase 3). When present, the engine
* mints signed authorization ops so a peer's ingest boundary can verify
* them. Constructed by hosts that have an identity; absent for
* identity-less repos, where no resolver is wired either.
*/
signingMaterial?: {
privateKey: string;
identityEntityId: string;
signedWith: string;
};
} & Partial<TrellisVcsConfig>);
private readPersistedConfig;
private writePersistedConfig;
private indexExistingFiles;
/**
* Initialize a new TrellisVCS repo. Creates .trellis/ directory and config.
*/
initRepo(opts?: InitRepoOptions): Promise<InitRepoResult>;
/**
* Open an existing TrellisVCS repo. Loads ops and replays into EAV store.
*/
open(): {
opsReplayed: number;
};
/**
* Index all untracked files currently on disk into the Trellis graph.
*/
indexWorkspace(opts?: {
onProgress?: (progress: InitProgress) => void;
}): Promise<IndexWorkspaceResult>;
/**
* Start watching the filesystem for changes.
*/
watch(opts?: {
reconcileExisting?: boolean;
}): void;
private getWatcherRoot;
/**
* Directory where agents should run tests and edit files for a lane.
* Uses the lane worktree when `lanes.worktreeBind` is enabled.
*/
getEditRoot(laneId?: string): string;
private isWorktreeBindEnabled;
private rebindWatcher;
private startWatcherAt;
private provisionLaneWorktree;
/**
* Auto-save a lane worktree before use (ADR 0038).
*
* Git is the sole authority over file bytes. Committing whatever the agent
* left in the worktree — even when the agent never ran an explicit git
* commit — guarantees re-entry and promotion see exactly the bytes the
* agent produced. No op-log blobs are materialized over disk.
*/
private materializeLaneWorktree;
private removeLaneWorktree;
/**
* Stop watching.
*/
stop(): void;
/**
* Returns all ops in the causal stream.
*/
getOps(): VcsOp[];
/**
* Integrate externally supplied ops exactly as received.
*
* This is the sync ingestion primitive: callers are responsible for
* exchanging, validating, and ordering ops before handing them to the
* engine. The engine dedupes by hash, materializes each new op, and avoids
* creating local branch-advance follow-up ops for remote history.
*/
integrateOps(ops: VcsOp[]): Promise<IntegrateOpsResult>;
/**
* Returns the total number of ops.
*/
getOpCount(): number;
/**
* Returns the EAV store for direct querying.
*/
getStore(): EAVStore;
/**
* Returns the blob store for content retrieval.
*/
getBlobStore(): BlobStore | null;
/**
* Returns the blob resolver (wraps BlobStore with git fallback).
*/
getBlobResolver(): BlobResolver | null;
/**
* Returns the current status: tracked files, last op, branch info.
*/
status(): {
branch: string;
totalOps: number;
trackedFiles: number;
lastOp: VcsOp | undefined;
recentOps: VcsOp[];
};
/**
* Returns op history, optionally filtered by file path.
*/
log(opts?: {
limit?: number;
filePath?: string;
}): VcsOp[];
/**
* Returns all tracked file paths and their content hashes.
*/
trackedFiles(): Array<{
path: string;
contentHash: string | undefined;
}>;
/**
* Returns the root path of the repository.
*/
getRootPath(): string;
/**
* Checks if a .trellis directory exists at the root path.
*/
static isRepo(rootPath: string): boolean;
static repair(rootPath: string, opts?: import('./vcs/op-log.js').RepairOptions): import('./vcs/op-log.js').RepairResult;
createBranch(name: string): Promise<VcsOp>;
switchBranch(name: string): void;
listBranches(): branchMod.BranchInfo[];
deleteBranch(name: string): Promise<VcsOp>;
getCurrentBranch(): string;
/**
* Integration branch head op hash from the materialized store (ADR 0004).
* Pass `principal` to resolve a single writer's per-principal ref zone
* (ADR 0022 §4) — two writers on the same personal branch keep separate heads.
*/
getBranchHeadOpHash(branchName?: string, principal?: string): string | undefined;
/**
* Engine context for the zone capability module (ADR 0022).
*
* Capability writes mint ops through this rather than touching the store,
* so grants survive a reboot, replicate to peers, and are hash-covered.
*/
capabilityContext(): EngineContext;
getActiveLaneId(): string | undefined;
/**
* Write a re-entry checkpoint (`.trellis/reentry-checkpoint.json`) capturing
* the active lane's issue — the harness-side "session end" bookkeeping.
* Never promotes; checkpointing is always safe.
*/
writeReentryCheckpoint(): ReentryCheckpoint;
/**
* Re-entry status for the harness "whereami" banner: the persisted
* checkpoint (if any) plus the active lane's issue.
*/
reentryStatus(): {
checkpoint: ReentryCheckpoint | null;
activeLaneId?: string;
issueIds: string[];
};
/**
* Persist a session's LLM usage rollup as a `session:<id>` EAV entity
* (latest write wins). Store-only — no op journal pollution. Backs the
* harness token/cost visibility (Phase 2).
*/
recordSessionUsage(input: {
sessionId: string;
laneId?: string;
tokens: number;
inputTokens?: number;
outputTokens?: number;
cost?: number;
model?: string;
}): void;
/** Read back a session usage rollup, if recorded. */
getSessionUsage(sessionId: string): Record<string, unknown> | null;
/** Whether milestones should auto-commit to git on create (config opt-in). */
get milestoneAutoCommit(): boolean;
/** Last enter/leave/open materialization counters (W4). */
getMaterializationStats(): MaterializationStats;
listLanes(): LaneMeta[];
getIntegrationOpCount(): number;
getLaneOpCount(laneId: string): number;
getLaneMeta(laneId: string): LaneMeta | undefined;
/**
* Prune worktrees for lanes that haven't been updated in N days.
* Skips active lanes and lanes without worktrees.
*/
pruneStaleWorktrees(): {
pruned: number;
skipped: number;
};
/** Active lane linked to an issue, if any. */
findLaneForIssue(issueId: string): LaneMeta | undefined;
/** Active lane bound to a Cursor/agent session id. */
findLaneForSession(sessionId: string): LaneMeta | undefined;
/**
* Find or create a lane for a session. Used by Cursor hooks for tab isolation.
*/
ensureSessionLane(opts: {
sessionId: string;
issueId?: string;
enter?: boolean;
}): Promise<LaneMeta>;
/**
* Journal the current working tree into the integration op-log.
*
* Reconciles the *actual* files on disk against the op-log's recorded file
* state (`buildFileStateAtOp`) and emits `vcs:fileAdd` / `vcs:fileModify`
* ops for every divergence, then advances `branch` (the git-sync target) to
* the last journaled op so a subsequent materialize sees the reconciled
* state. This closes the journaling gap that let an un-journaled working
* tree get clobbered by `git sync` materialization: after catch-up, the
* op-log file state matches disk, so a subsequent materialize is a no-op
* instead of a revert.
*
* Returns the count of ops journaled and any paths that could not be
* reconciled (unreadable, blob-store failure). Callers that require a safe
* materialize should refuse when `unreconciled.length > 0`.
*/
journalWorkingTreeToOps(opts?: {
branch?: string;
onProgress?: (progress: {
phase: 'scanning' | 'journaling' | 'done';
current: number;
total: number;
message: string;
}) => void;
}): Promise<{
journaled: number;
unreconciled: string[];
}>;
/**
* Commit the actual working tree to the default git branch (ADR 0038).
*
* Git is the sole authority over file bytes. The working tree is staged and
* committed as-is — the op-log is never consulted for file content and no
* op-log state is materialized over disk. After a successful commit a
* non-materializing `vcs:gitSync` annotation op records `{ gitCommitHash,
* gitBranch }` so the op-log can answer "where do these bytes live in git?"
* without owning them.
*/
syncGitIntegration(opts?: {
message?: string;
push?: boolean;
lane?: LaneMeta;
laneOps?: VcsOp[];
/** When true, sync even if git.syncOnPromote is false. */
force?: boolean;
}): Promise<GitSyncResult>;
/**
* Record a non-materializing `vcs:gitSync` annotation (ADR 0038): the op-log
* learns where bytes live in git without ever claiming byte authority.
*/
private recordGitSyncAnnotation;
/**
* Deliver a promoted lane's bytes to git (ADR 0038).
*
* 1. Auto-commit the lane worktree (the agent's actual bytes) onto its
* `lane/<shortId>` branch.
* 2. Merge that branch into the integration head of the main worktree.
* 3. Root sync: commit any remaining root dirt + push when configured.
*
* A git merge conflict fails the delivery — git is the authority, so a
* conflicted merge cannot be papered over with a synthesized file state.
*/
private gitDeliveryForLane;
/**
* Enter lane from TRELLIS_LANE_ID when set (hooks/MCP/subprocess agents).
*/
syncEnvLaneFromEnv(): Promise<void>;
/** Ops and touched files in a lane journal (for `trellis lane diff`). */
summarizeLane(laneId: string): {
meta: LaneMeta;
ops: VcsOp[];
filePaths: string[];
integrationHead?: string;
coherence: laneCoherenceMod.LaneCoherence;
};
/**
* Fork a new agent lane from the current integration branch head.
* Writes `vcs:laneCreate` to the integration journal only.
*/
createLane(opts?: {
fromBranch?: string;
targetBranch?: string;
issueId?: string;
sessionId?: string;
worktreePath?: string;
name?: string;
parentLaneId?: string;
forkKind?: laneMod.LaneForkKind;
}): Promise<LaneMeta>;
/**
* Open a fresh domain-scoped lane and enter it (TRL-117).
* Leaves the current lane if any. Does not require an issue — promote
* boundary is the new lane itself. Parent lineage is recorded as sibling.
*/
splitLane(opts?: {
name?: string;
fromBranch?: string;
sessionId?: string;
}): Promise<{
meta: LaneMeta;
splitFrom?: string;
}>;
forkLane(parentLaneId: string, opts?: {
sessionId?: string;
issueId?: string;
worktreePath?: string;
forkKind?: laneMod.LaneForkKind;
}): Promise<LaneMeta>;
/**
* Enter a lane: route subsequent writes to its isolated journal.
*/
enterLane(laneId: string): Promise<LaneMeta>;
/** Leave the active lane and restore integration-only materialized state. */
leaveLane(): Promise<void>;
/** Mark a lane dropped (leaves first if it is the active lane). */
dropLane(laneId: string): Promise<void>;
recordLaneGc(entries: {
laneId: string;
disposition: string;
reason: string;
}[]): Promise<void>;
promoteLane(laneId: string, opts?: {
dryRun?: boolean;
explain?: boolean;
toBranch?: string;
requireTest?: boolean;
/** Break a stale or abandoned promote lock (dangerous if another promote is live). */
forceLock?: boolean;
/** Milestone narrative (TRL-117). Auto-drafted when omitted unless milestone:false. */
message?: string;
/** Set false to promote without creating a milestone. Default true. */
milestone?: boolean;
}): Promise<LanePromoteResult>;
/**
* Promote active issue lane before close when it has unpromoted journal ops.
* No-ops when the lane has nothing replayable onto integration (e.g. only
* testRun / claim metadata) — that still satisfies the promote boundary.
*/
private autoPromoteIssueLaneBeforeClose;
createMilestone(message: string, opts?: {
fromOpHash?: string;
toOpHash?: string;
}): Promise<VcsOp>;
listMilestones(): milestoneMod.MilestoneInfo[];
createCheckpoint(trigger?: checkpointMod.CheckpointTrigger): Promise<VcsOp>;
listCheckpoints(): checkpointMod.CheckpointInfo[];
setCheckpointThreshold(threshold: number): void;
/**
* Diff two branches by comparing their file states.
*/
diffBranches(branchA: string, branchB: string): diffMod.DiffResult;
/**
* Diff between two op hashes in the causal stream.
*/
diffOps(fromHash: string, toHash: string): diffMod.DiffResult;
/**
* Diff the current state against a specific op hash (e.g. a milestone).
*/
diffFromOp(opHash: string): diffMod.DiffResult;
/**
* Three-way merge: merge source branch state into current branch state.
* Uses the fork-point (branch creation op) as the common ancestor.
*/
mergeBranch(sourceBranch: string): mergeMod.MergeResult;
private _parsers;
/**
* Parse a file's content into AST-level entities.
*/
parseFile(content: string, filePath: string): ParseResult | null;
/**
* Compute semantic diff between two versions of a file.
*/
semanticDiff(oldContent: string, newContent: string, filePath: string): SemanticPatch[];
private _garden;
/**
* Get the Idea Garden instance for exploring abandoned work.
*/
garden(): IdeaGarden;
createIssue(title: string, opts?: issueMod.IssueCreateOptions): Promise<VcsOp>;
updateIssue(id: string, updates: {
title?: string;
description?: string;
priority?: 'critical' | 'high' | 'medium' | 'low';
labels?: string[];
assignee?: string;
status?: 'backlog' | 'queue' | 'in_progress' | 'paused' | 'closed';
parentId?: string | null;
}): Promise<VcsOp>;
/**
* Start an issue: optionally create+enter a lane, optionally create+switch to
* a branch, emit `vcs:issueStart`, apply start criteria.
*
* `branch` is separable from `lane` on purpose. Branch creation used to be
* unconditional while the lane was opt-out — so a repo that treats branches as
* an antipattern (staying on `main`) had to avoid `issue start` entirely, and
* avoiding it silently opted every agent out of LANES too, since this is the
* only thing that creates one. Agents then shared the main tree and swept each
* other's in-flight edits. The lane is the isolation that matters; the branch
* is a naming convenience.
*/
startIssue(id: string, opts?: {
lane?: boolean;
branch?: boolean;
sessionId?: string;
}): Promise<VcsOp>;
pauseIssue(id: string, note: string): Promise<VcsOp>;
resumeIssue(id: string, opts?: {
lane?: boolean;
sessionId?: string;
}): Promise<VcsOp>;
closeIssue(id: string, opts?: {
confirm?: boolean;
push?: boolean;
noPromote?: boolean;
requireTest?: boolean;
}): Promise<{
op?: VcsOp;
criteriaResults: issueMod.CriterionResult[];
gitSync?: GitSyncResult;
promoteResult?: lanePromoteMod.LanePromoteResult;
}>;
triageIssue(id: string): Promise<VcsOp>;
reopenIssue(id: string): Promise<VcsOp>;
checkCompletionReadiness(): issueMod.CompletionReadiness;
assignIssue(id: string, agentId: string): Promise<VcsOp>;
blockIssue(id: string, blockedById: string): Promise<VcsOp>;
unblockIssue(id: string, blockedById: string): Promise<VcsOp>;
addCriterion(issueId: string, description: string, opts?: string | {
command?: string;
suite?: string;
}): Promise<VcsOp>;
/** Retract an acceptance criterion by its 1-based index in the live list (TRL-1). */
removeCriterion(issueId: string, criterionIndex: number): Promise<VcsOp>;
setCriterionStatus(issueId: string, criterionIndex: number, status: 'passed' | 'failed' | 'pending'): Promise<VcsOp>;
runCriteria(issueId: string): Promise<issueMod.CriterionResult[]>;
runTests(opts?: {
suiteIds?: string[];
laneId?: string;
issueId?: string;
trigger?: testRunnerMod.TestRunTrigger;
}): Promise<testRunnerMod.TestRunResult[]>;
listIssues(filters?: issueMod.IssueFilters): issueMod.IssueInfo[];
getIssue(id: string): issueMod.IssueInfo | null;
getActiveIssues(): issueMod.IssueInfo[];
createStoreEntity(entityId: string, type: string, attributes?: Record<string, Atom>, opts?: storeMod.StoreEntityCreateOptions): Promise<VcsOp>;
updateStoreEntity(entityId: string, updates: Record<string, Atom>): Promise<VcsOp>;
deleteStoreEntity(entityId: string): Promise<VcsOp>;
getStoreEntity(entityId: string): EntityRecord | null;
listStoreEntities(type?: string, filters?: Record<string, Atom>, opts?: {
includeVcs?: boolean;
}): EntityRecord[];
/** Raw EAV store (materialized from ops.json in VCS repos). */
getEavStore(): EAVStore;
addStoreFact(entityId: string, attribute: string, value: Atom): Promise<VcsOp>;
removeStoreFact(entityId: string, attribute: string, value: Atom): Promise<VcsOp>;
addStoreLink(sourceId: string, attribute: string, targetId: string): Promise<VcsOp>;
removeStoreLink(sourceId: string, attribute: string, targetId: string): Promise<VcsOp>;
recordDecision(input: decisionMod.DecisionInput): Promise<VcsOp>;
/**
* Record a harness chat message as a vcs:chatMessage op.
*
* Config-gated by `transcripts.enabled` (default false); returns null when
* disabled. Transcript ops are local-only by default (no peer sync).
*/
recordChatMessage(input: transcriptMod.ChatMessageInput): Promise<VcsOp | null>;
/** Recent chat messages from the op log (most recent first). */
listChatMessages(opts?: {
sessionId?: string;
laneId?: string;
limit?: number;
}): Promise<ReturnType<typeof transcriptMod.listChatMessages>>;
recordRemotePush(info: {
remoteName?: string;
remoteRepoId?: string;
remoteTailHash?: string;
remoteByteLength?: number;
}): Promise<VcsOp>;
recordRemotePull(info: {
remoteName?: string;
remoteRepoId?: string;
remoteTailHash?: string;
remoteByteLength?: number;
}): Promise<VcsOp>;
/** Set (or update) the project's owner/name/kind metadata and persist it. */
setProjectMetadata(meta: NonNullable<TrellisVcsConfig['project']>): void;
/** The stable ledger repoId (persisted, or the in-memory config value). */
getPersistedRepoId(): string;
getProjectMetadata(): NonNullable<TrellisVcsConfig['project']>;
/**
* Mint + apply the owner-signed `vcs:repoAttest` op (ADR 0032 §4).
* Returns the applied op. The attestation is chained to the current tail.
*/
attestProject(input: {
owner: string;
repoName: string;
repoId: string;
kind?: string;
privateKey: string;
}): Promise<VcsOp>;
queryDecisions(filter?: decisionMod.DecisionFilter): decisionMod.Decision[];
getDecisionChain(entityId: string): decisionMod.Decision[];
getDecision(id: string): decisionMod.Decision | null;
private _ctx;
private trellisDir;
private getActiveJournal;
private invalidateIntegrationCache;
private loadLaneJournalOps;
private refreshMaterializedStore;
/** Swap back to cached integration store without replaying the journal. */
private restoreIntegrationOnlyStore;
private rebuildStore;
private syncIngestionLastOpHash;
/**
* Tag an op with the lane it was minted in (TRL-102).
*
* Writes the ENVELOPE field, not `op.vcs`. `createVcsOp` has already hashed
* `vcs` by the time we get here, so mutating the payload silently invalidated
* the hash — every op in every lane journal failed `verifyVcsOpHash`, and
* would be rejected as `hash-mismatch` at any ingest boundary.
*
* The lane is ambient context, not identity: the same semantic op in two
* lanes must hash identically, or peers lose dedup and cherry-pick rewrites
* identity. `laneId` is outside the preimage by construction.
*/
private stampLaneId;
private requireActiveLaneLog;
private isIssueIntegrationOp;
private applyOp;
private appendBranchAdvance;
private flushAutoCheckpoint;
private loadCurrentBranch;
private replayOp;
}
//# sourceMappingURL=engine.d.ts.map