UNPKG

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
/** * 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