trellis 1.0.7 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +533 -82
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Sync Engine
3
+ *
4
+ * DESIGN.md §10.5 — Peer sync protocol.
5
+ * Coordinates push/pull of ops between peers using a transport layer.
6
+ * Supports both linear (fast-forward only) and CRDT (concurrent append)
7
+ * branch modes.
8
+ */
9
+
10
+ import type { VcsOp } from '../vcs/types.js';
11
+ import type {
12
+ SyncTransport,
13
+ SyncMessage,
14
+ SyncState,
15
+ PeerId,
16
+ BranchPolicy,
17
+ } from './types.js';
18
+ import { reconcile, findForkPoint, type ReconcileResult } from './reconciler.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Sync Engine
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export class SyncEngine {
25
+ private localPeerId: string;
26
+ private state: SyncState;
27
+ private transport: SyncTransport;
28
+ private getLocalOps: () => VcsOp[];
29
+ private onOpsReceived: (ops: VcsOp[]) => void;
30
+ private branchPolicy: BranchPolicy;
31
+
32
+ constructor(opts: {
33
+ localPeerId: string;
34
+ transport: SyncTransport;
35
+ getLocalOps: () => VcsOp[];
36
+ onOpsReceived: (ops: VcsOp[]) => void;
37
+ branchPolicy?: BranchPolicy;
38
+ }) {
39
+ this.localPeerId = opts.localPeerId;
40
+ this.transport = opts.transport;
41
+ this.getLocalOps = opts.getLocalOps;
42
+ this.onOpsReceived = opts.onOpsReceived;
43
+ this.branchPolicy = opts.branchPolicy ?? { linear: true };
44
+
45
+ this.state = {
46
+ localPeerId: opts.localPeerId,
47
+ peerHeads: new Map(),
48
+ pendingAcks: new Set(),
49
+ lastSync: new Map(),
50
+ };
51
+
52
+ // Register message handler
53
+ this.transport.onMessage((msg) => this.handleMessage(msg));
54
+ }
55
+
56
+ // -------------------------------------------------------------------------
57
+ // Public API
58
+ // -------------------------------------------------------------------------
59
+
60
+ /**
61
+ * Initiate a sync with a specific peer.
62
+ * Sends a 'have' message advertising our heads.
63
+ */
64
+ async pushTo(peerId: string): Promise<void> {
65
+ const ops = this.getLocalOps();
66
+ const heads: Record<string, string> = {};
67
+ if (ops.length > 0) {
68
+ heads['main'] = ops[ops.length - 1].hash;
69
+ }
70
+
71
+ await this.transport.send(peerId, {
72
+ type: 'have',
73
+ peerId: this.localPeerId,
74
+ heads,
75
+ opCount: ops.length,
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Request ops from a peer.
81
+ */
82
+ async pullFrom(peerId: string): Promise<void> {
83
+ const ops = this.getLocalOps();
84
+ const lastHash = ops.length > 0 ? ops[ops.length - 1].hash : undefined;
85
+
86
+ await this.transport.send(peerId, {
87
+ type: 'want',
88
+ peerId: this.localPeerId,
89
+ wantHashes: [],
90
+ afterHash: lastHash,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Send all our ops to a peer (full push).
96
+ */
97
+ async sendOps(peerId: string, ops?: VcsOp[]): Promise<void> {
98
+ const opsToSend = ops ?? this.getLocalOps();
99
+ await this.transport.send(peerId, {
100
+ type: 'ops',
101
+ peerId: this.localPeerId,
102
+ ops: opsToSend,
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Reconcile our ops with a remote peer's ops.
108
+ */
109
+ reconcileWith(remoteOps: VcsOp[]): ReconcileResult {
110
+ const localOps = this.getLocalOps();
111
+ return reconcile(localOps, remoteOps);
112
+ }
113
+
114
+ /**
115
+ * Get current sync state.
116
+ */
117
+ getState(): SyncState {
118
+ return this.state;
119
+ }
120
+
121
+ /**
122
+ * Get branch policy.
123
+ */
124
+ getBranchPolicy(): BranchPolicy {
125
+ return this.branchPolicy;
126
+ }
127
+
128
+ /**
129
+ * Set branch policy.
130
+ */
131
+ setBranchPolicy(policy: BranchPolicy): void {
132
+ this.branchPolicy = policy;
133
+ }
134
+
135
+ /**
136
+ * List known peers.
137
+ */
138
+ listPeers(): PeerId[] {
139
+ return this.transport.peers();
140
+ }
141
+
142
+ // -------------------------------------------------------------------------
143
+ // Message handling
144
+ // -------------------------------------------------------------------------
145
+
146
+ private handleMessage(msg: SyncMessage): void {
147
+ switch (msg.type) {
148
+ case 'have':
149
+ this.handleHave(msg);
150
+ break;
151
+ case 'want':
152
+ this.handleWant(msg);
153
+ break;
154
+ case 'ops':
155
+ this.handleOps(msg);
156
+ break;
157
+ case 'ack':
158
+ this.handleAck(msg);
159
+ break;
160
+ }
161
+ }
162
+
163
+ private handleHave(msg: Extract<SyncMessage, { type: 'have' }>): void {
164
+ // Store peer heads
165
+ this.state.peerHeads.set(msg.peerId, msg.heads);
166
+
167
+ // Compare with our state — determine what we need
168
+ const localOps = this.getLocalOps();
169
+ const localHashes = new Set(localOps.map((o) => o.hash));
170
+
171
+ // Check if peer has ops we don't
172
+ for (const [, hash] of Object.entries(msg.heads)) {
173
+ if (!localHashes.has(hash)) {
174
+ // Peer is ahead — request their ops
175
+ this.transport.send(msg.peerId, {
176
+ type: 'want',
177
+ peerId: this.localPeerId,
178
+ wantHashes: [],
179
+ afterHash: localOps.length > 0 ? localOps[localOps.length - 1].hash : undefined,
180
+ });
181
+ return;
182
+ }
183
+ }
184
+
185
+ // Check if we have ops they don't — push them
186
+ const peerOpCount = msg.opCount;
187
+ if (localOps.length > peerOpCount) {
188
+ // Send ops they might be missing
189
+ const opsToSend = localOps.slice(peerOpCount);
190
+ this.transport.send(msg.peerId, {
191
+ type: 'ops',
192
+ peerId: this.localPeerId,
193
+ ops: opsToSend,
194
+ });
195
+ }
196
+ }
197
+
198
+ private handleWant(msg: Extract<SyncMessage, { type: 'want' }>): void {
199
+ const localOps = this.getLocalOps();
200
+
201
+ let opsToSend: VcsOp[];
202
+ if (msg.afterHash) {
203
+ const idx = localOps.findIndex((o) => o.hash === msg.afterHash);
204
+ opsToSend = idx >= 0 ? localOps.slice(idx + 1) : localOps;
205
+ } else if (msg.wantHashes.length > 0) {
206
+ const wanted = new Set(msg.wantHashes);
207
+ opsToSend = localOps.filter((o) => wanted.has(o.hash));
208
+ } else {
209
+ opsToSend = localOps;
210
+ }
211
+
212
+ if (opsToSend.length > 0) {
213
+ this.transport.send(msg.peerId, {
214
+ type: 'ops',
215
+ peerId: this.localPeerId,
216
+ ops: opsToSend,
217
+ });
218
+ }
219
+ }
220
+
221
+ private handleOps(msg: Extract<SyncMessage, { type: 'ops' }>): void {
222
+ if (msg.ops.length === 0) return;
223
+
224
+ if (this.branchPolicy.linear) {
225
+ // Linear mode: only accept fast-forward appends
226
+ const localOps = this.getLocalOps();
227
+ const localHashes = new Set(localOps.map((o) => o.hash));
228
+
229
+ // Filter to only new ops
230
+ const newOps = msg.ops.filter((o) => !localHashes.has(o.hash));
231
+ if (newOps.length > 0) {
232
+ this.onOpsReceived(newOps);
233
+ }
234
+ } else {
235
+ // CRDT mode: reconcile divergent streams
236
+ const result = this.reconcileWith(msg.ops);
237
+ if (result.uniqueToB.length > 0) {
238
+ this.onOpsReceived(result.uniqueToB);
239
+ }
240
+ }
241
+
242
+ // Acknowledge
243
+ this.transport.send(msg.peerId, {
244
+ type: 'ack',
245
+ peerId: this.localPeerId,
246
+ integrated: msg.ops.map((o) => o.hash),
247
+ });
248
+
249
+ this.state.lastSync.set(msg.peerId, new Date().toISOString());
250
+ }
251
+
252
+ private handleAck(msg: Extract<SyncMessage, { type: 'ack' }>): void {
253
+ for (const hash of msg.integrated) {
254
+ this.state.pendingAcks.delete(hash);
255
+ }
256
+ this.state.lastSync.set(msg.peerId, new Date().toISOString());
257
+ }
258
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Peer Sync — Type Definitions
3
+ *
4
+ * DESIGN.md §3.5, §10.5 — Peer sync + CRDTs.
5
+ * Types for peer identity, sync messages, causal DAG, and
6
+ * branch concurrency modes.
7
+ */
8
+
9
+ import type { VcsOp } from '../vcs/types.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Peer Identity
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export interface PeerId {
16
+ /** Unique peer identifier (typically derived from identity DID). */
17
+ id: string;
18
+ /** Human-readable display name. */
19
+ name: string;
20
+ /** Last seen timestamp. */
21
+ lastSeen?: string;
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Sync Messages
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export type SyncMessage =
29
+ | SyncHaveMessage
30
+ | SyncWantMessage
31
+ | SyncOpsMessage
32
+ | SyncAckMessage;
33
+
34
+ /** Advertise which op hashes we have. */
35
+ export interface SyncHaveMessage {
36
+ type: 'have';
37
+ peerId: string;
38
+ /** Our head op hashes (one per branch). */
39
+ heads: Record<string, string>;
40
+ /** Total op count for quick comparison. */
41
+ opCount: number;
42
+ }
43
+
44
+ /** Request ops we're missing. */
45
+ export interface SyncWantMessage {
46
+ type: 'want';
47
+ peerId: string;
48
+ /** Op hashes we need (those the remote has but we don't). */
49
+ wantHashes: string[];
50
+ /** Alternatively: request all ops after a given hash. */
51
+ afterHash?: string;
52
+ }
53
+
54
+ /** Send a batch of ops. */
55
+ export interface SyncOpsMessage {
56
+ type: 'ops';
57
+ peerId: string;
58
+ ops: VcsOp[];
59
+ }
60
+
61
+ /** Acknowledge receipt. */
62
+ export interface SyncAckMessage {
63
+ type: 'ack';
64
+ peerId: string;
65
+ /** Hashes of ops we've integrated. */
66
+ integrated: string[];
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Sync State
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export interface SyncState {
74
+ /** Our peer identity. */
75
+ localPeerId: string;
76
+ /** Known peers and their head hashes. */
77
+ peerHeads: Map<string, Record<string, string>>;
78
+ /** Ops we've sent but not yet acknowledged. */
79
+ pendingAcks: Set<string>;
80
+ /** Last sync timestamp per peer. */
81
+ lastSync: Map<string, string>;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Branch Concurrency Policy
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export interface BranchPolicy {
89
+ /** If true, only fast-forward appends (one writer). Default. */
90
+ linear: boolean;
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Sync Transport (abstract interface)
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export interface SyncTransport {
98
+ /** Send a message to a specific peer. */
99
+ send(peerId: string, message: SyncMessage): Promise<void>;
100
+ /** Register a handler for incoming messages. */
101
+ onMessage(handler: (message: SyncMessage) => void): void;
102
+ /** List connected peers. */
103
+ peers(): PeerId[];
104
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Content-Addressable Blob Store
3
+ *
4
+ * Stores file content indexed by SHA-256 hash. Provides the source of truth
5
+ * for file reconstruction at any point in history. The EAV graph stores
6
+ * structural metadata; the blob store stores byte-exact content.
7
+ *
8
+ * Storage format: `.trellis/blobs/{hash}` files on disk.
9
+ * Future: migrate to SQLite `blobs(hash TEXT PRIMARY KEY, content BLOB)`.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ export class BlobStore {
16
+ private blobDir: string;
17
+
18
+ constructor(trellisDir: string) {
19
+ this.blobDir = join(trellisDir, 'blobs');
20
+ if (!existsSync(this.blobDir)) {
21
+ mkdirSync(this.blobDir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Store content and return its SHA-256 hash.
27
+ * Idempotent — storing the same content twice is a no-op.
28
+ */
29
+ async put(content: Buffer | Uint8Array): Promise<string> {
30
+ const hash = await this.hash(content);
31
+ const blobPath = join(this.blobDir, hash);
32
+ if (!existsSync(blobPath)) {
33
+ writeFileSync(blobPath, content);
34
+ }
35
+ return hash;
36
+ }
37
+
38
+ /**
39
+ * Synchronous put — uses Bun's sync crypto if available.
40
+ */
41
+ putSync(content: Buffer | Uint8Array): string {
42
+ const hash = this.hashSync(content);
43
+ const blobPath = join(this.blobDir, hash);
44
+ if (!existsSync(blobPath)) {
45
+ writeFileSync(blobPath, content);
46
+ }
47
+ return hash;
48
+ }
49
+
50
+ /**
51
+ * Retrieve content by hash. Returns null if not found.
52
+ */
53
+ get(hash: string): Buffer | null {
54
+ const blobPath = join(this.blobDir, hash);
55
+ if (!existsSync(blobPath)) {
56
+ return null;
57
+ }
58
+ return readFileSync(blobPath);
59
+ }
60
+
61
+ /**
62
+ * Check if a blob exists.
63
+ */
64
+ has(hash: string): boolean {
65
+ return existsSync(join(this.blobDir, hash));
66
+ }
67
+
68
+ /**
69
+ * Compute SHA-256 hash of content (async).
70
+ */
71
+ async hash(content: Buffer | Uint8Array): Promise<string> {
72
+ const hashBuffer = await crypto.subtle.digest(
73
+ 'SHA-256',
74
+ content as unknown as ArrayBuffer,
75
+ );
76
+ return this.hexFromBuffer(hashBuffer);
77
+ }
78
+
79
+ /**
80
+ * Compute SHA-256 hash of content (sync, using Bun's CryptoHasher).
81
+ */
82
+ hashSync(content: Buffer | Uint8Array): string {
83
+ const hasher = new Bun.CryptoHasher('sha256');
84
+ hasher.update(content);
85
+ return hasher.digest('hex');
86
+ }
87
+
88
+ /**
89
+ * Returns the number of blobs stored.
90
+ */
91
+ count(): number {
92
+ try {
93
+ const { readdirSync } = require('fs');
94
+ return readdirSync(this.blobDir).length;
95
+ } catch {
96
+ return 0;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Returns the total size of all blobs in bytes.
102
+ */
103
+ totalSize(): number {
104
+ try {
105
+ const { readdirSync, statSync } = require('fs');
106
+ const files: string[] = readdirSync(this.blobDir);
107
+ return files.reduce((sum: number, f: string) => {
108
+ try {
109
+ return sum + statSync(join(this.blobDir, f)).size;
110
+ } catch {
111
+ return sum;
112
+ }
113
+ }, 0);
114
+ } catch {
115
+ return 0;
116
+ }
117
+ }
118
+
119
+ private hexFromBuffer(buffer: ArrayBuffer): string {
120
+ return Array.from(new Uint8Array(buffer))
121
+ .map((b) => b.toString(16).padStart(2, '0'))
122
+ .join('');
123
+ }
124
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Branch Management Module
3
+ *
4
+ * Extracted from engine.ts per DESIGN.md §8.1.
5
+ * Handles create, switch, list, delete branch operations.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { createVcsOp } from './ops.js';
11
+ import type { VcsOp } from './types.js';
12
+ import type { EngineContext } from './engine-context.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Types
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface BranchInfo {
19
+ name: string;
20
+ isCurrent: boolean;
21
+ createdAt?: string;
22
+ }
23
+
24
+ export interface BranchState {
25
+ currentBranch: string;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Operations
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * Create a new branch forked from the current branch.
34
+ */
35
+ export async function createBranch(
36
+ ctx: EngineContext,
37
+ name: string,
38
+ currentBranch: string,
39
+ ): Promise<VcsOp> {
40
+ const existing = ctx.store
41
+ .getFactsByAttribute('type')
42
+ .filter((f) => f.v === 'Branch' && f.e === `branch:${name}`);
43
+ if (existing.length > 0) {
44
+ throw new Error(`Branch '${name}' already exists`);
45
+ }
46
+
47
+ const op = await createVcsOp('vcs:branchCreate', {
48
+ agentId: ctx.agentId,
49
+ previousHash: ctx.getLastOp()?.hash,
50
+ vcs: {
51
+ branchName: name,
52
+ baseBranch: currentBranch,
53
+ targetOpHash: ctx.getLastOp()?.hash,
54
+ },
55
+ });
56
+ ctx.applyOp(op);
57
+ return op;
58
+ }
59
+
60
+ /**
61
+ * Switch to an existing branch.
62
+ */
63
+ export function switchBranch(
64
+ ctx: EngineContext,
65
+ name: string,
66
+ ): void {
67
+ const branchFacts = ctx.store
68
+ .getFactsByEntity(`branch:${name}`)
69
+ .filter((f) => f.a === 'type' && f.v === 'Branch');
70
+ if (branchFacts.length === 0) {
71
+ throw new Error(`Branch '${name}' does not exist`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * List all branches.
77
+ */
78
+ export function listBranches(
79
+ ctx: EngineContext,
80
+ currentBranch: string,
81
+ ): BranchInfo[] {
82
+ const branchFacts = ctx.store
83
+ .getFactsByAttribute('type')
84
+ .filter((f) => f.v === 'Branch');
85
+
86
+ return branchFacts.map((f) => {
87
+ const nameFact = ctx.store
88
+ .getFactsByEntity(f.e)
89
+ .find((ef) => ef.a === 'name');
90
+ const createdFact = ctx.store
91
+ .getFactsByEntity(f.e)
92
+ .find((ef) => ef.a === 'createdAt');
93
+ const name = (nameFact?.v as string) ?? f.e.replace('branch:', '');
94
+ return {
95
+ name,
96
+ isCurrent: name === currentBranch,
97
+ createdAt: createdFact?.v as string | undefined,
98
+ };
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Delete a branch (cannot delete the current branch).
104
+ */
105
+ export async function deleteBranch(
106
+ ctx: EngineContext,
107
+ name: string,
108
+ currentBranch: string,
109
+ ): Promise<VcsOp> {
110
+ if (name === currentBranch) {
111
+ throw new Error(`Cannot delete the current branch '${name}'`);
112
+ }
113
+ const branchFacts = ctx.store
114
+ .getFactsByEntity(`branch:${name}`)
115
+ .filter((f) => f.a === 'type' && f.v === 'Branch');
116
+ if (branchFacts.length === 0) {
117
+ throw new Error(`Branch '${name}' does not exist`);
118
+ }
119
+
120
+ const op = await createVcsOp('vcs:branchDelete', {
121
+ agentId: ctx.agentId,
122
+ previousHash: ctx.getLastOp()?.hash,
123
+ vcs: { branchName: name },
124
+ });
125
+ ctx.applyOp(op);
126
+ return op;
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Persistence
131
+ // ---------------------------------------------------------------------------
132
+
133
+ export function saveBranchState(rootPath: string, state: BranchState): void {
134
+ const statePath = join(rootPath, '.trellis', 'state.json');
135
+ writeFileSync(statePath, JSON.stringify(state));
136
+ }
137
+
138
+ export function loadBranchState(rootPath: string): BranchState {
139
+ const statePath = join(rootPath, '.trellis', 'state.json');
140
+ if (existsSync(statePath)) {
141
+ try {
142
+ const raw = readFileSync(statePath, 'utf-8');
143
+ const state = JSON.parse(raw);
144
+ if (state.currentBranch) {
145
+ return { currentBranch: state.currentBranch };
146
+ }
147
+ } catch {}
148
+ }
149
+ return { currentBranch: 'main' };
150
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Checkpoint Module
3
+ *
4
+ * Extracted from engine.ts per DESIGN.md §8.1.
5
+ * Handles checkpoint creation, listing, and auto-checkpoint logic.
6
+ */
7
+
8
+ import { createVcsOp } from './ops.js';
9
+ import type { VcsOp } from './types.js';
10
+ import type { EngineContext } from './engine-context.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Types
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export type CheckpointTrigger = 'manual' | 'op-count' | 'interval' | 'green-build';
17
+
18
+ export interface CheckpointInfo {
19
+ id: string;
20
+ createdAt?: string;
21
+ trigger?: string;
22
+ atOpHash?: string;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Operations
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Create a checkpoint at the current position in the causal stream.
31
+ */
32
+ export async function createCheckpoint(
33
+ ctx: EngineContext,
34
+ trigger: CheckpointTrigger = 'manual',
35
+ ): Promise<VcsOp> {
36
+ const op = await createVcsOp('vcs:checkpointCreate', {
37
+ agentId: ctx.agentId,
38
+ previousHash: ctx.getLastOp()?.hash,
39
+ vcs: { trigger },
40
+ });
41
+ ctx.applyOp(op);
42
+ return op;
43
+ }
44
+
45
+ /**
46
+ * List all checkpoints from the EAV store.
47
+ */
48
+ export function listCheckpoints(ctx: EngineContext): CheckpointInfo[] {
49
+ const cpFacts = ctx.store
50
+ .getFactsByAttribute('type')
51
+ .filter((f) => f.v === 'Checkpoint');
52
+
53
+ return cpFacts.map((f) => {
54
+ const facts = ctx.store.getFactsByEntity(f.e);
55
+ const get = (attr: string) =>
56
+ facts.find((ef) => ef.a === attr)?.v as string | undefined;
57
+ return {
58
+ id: f.e,
59
+ createdAt: get('createdAt'),
60
+ trigger: get('trigger'),
61
+ atOpHash: get('atOpHash'),
62
+ };
63
+ });
64
+ }