trellis 2.0.13 → 2.1.2

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 (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
package/src/ui/server.ts DELETED
@@ -1,419 +0,0 @@
1
- /**
2
- * Trellis UI Server
3
- *
4
- * Lightweight Bun HTTP server that exposes the TrellisVCS engine
5
- * as a JSON API and serves a single-file force-graph visualization.
6
- *
7
- * Endpoints:
8
- * GET / → client.html
9
- * GET /api/graph → full graph (nodes + edges)
10
- * GET /api/search → semantic search (?q=...&limit=10&type=...)
11
- * GET /api/node/:id → node detail
12
- */
13
-
14
- import { readFileSync, existsSync } from 'fs';
15
- import { join, dirname } from 'path';
16
- import { TrellisVcsEngine } from '../engine.js';
17
- import {
18
- buildRefIndex,
19
- createResolverContext,
20
- getOutgoingRefs,
21
- getReferencedEntities,
22
- getBacklinks,
23
- } from '../links/index.js';
24
-
25
- // ---------------------------------------------------------------------------
26
- // Types
27
- // ---------------------------------------------------------------------------
28
-
29
- export interface GraphNode {
30
- id: string;
31
- label: string;
32
- type: 'file' | 'milestone' | 'issue' | 'branch';
33
- meta: Record<string, unknown>;
34
- }
35
-
36
- export interface GraphEdge {
37
- source: string;
38
- target: string;
39
- type: 'milestone_file' | 'issue_branch' | 'wikilink' | 'causal';
40
- label?: string;
41
- }
42
-
43
- export interface GraphData {
44
- nodes: GraphNode[];
45
- edges: GraphEdge[];
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // Graph builder
50
- // ---------------------------------------------------------------------------
51
-
52
- function buildGraph(engine: TrellisVcsEngine): GraphData {
53
- const nodes: GraphNode[] = [];
54
- const edges: GraphEdge[] = [];
55
- const nodeIds = new Set<string>();
56
-
57
- // --- Files ---
58
- const files = engine.trackedFiles();
59
- for (const f of files) {
60
- const id = `file:${f.path}`;
61
- nodes.push({
62
- id,
63
- label: f.path,
64
- type: 'file',
65
- meta: { contentHash: f.contentHash },
66
- });
67
- nodeIds.add(id);
68
- }
69
-
70
- // --- Milestones ---
71
- const milestones = engine.listMilestones();
72
- for (const m of milestones) {
73
- const id = `milestone:${m.id}`;
74
- nodes.push({
75
- id,
76
- label: m.message ?? m.id,
77
- type: 'milestone',
78
- meta: {
79
- createdAt: m.createdAt,
80
- affectedFiles: m.affectedFiles,
81
- fromOpHash: m.fromOpHash,
82
- toOpHash: m.toOpHash,
83
- },
84
- });
85
- nodeIds.add(id);
86
-
87
- // Edges: milestone → affected files
88
- for (const fp of m.affectedFiles ?? []) {
89
- const fileId = `file:${fp}`;
90
- if (nodeIds.has(fileId)) {
91
- edges.push({
92
- source: id,
93
- target: fileId,
94
- type: 'milestone_file',
95
- });
96
- }
97
- }
98
- }
99
-
100
- // --- Issues ---
101
- const issues = engine.listIssues();
102
- for (const iss of issues) {
103
- const id = `issue:${iss.id}`;
104
- nodes.push({
105
- id,
106
- label: iss.title ?? iss.id,
107
- type: 'issue',
108
- meta: {
109
- status: iss.status,
110
- priority: iss.priority,
111
- labels: iss.labels,
112
- assignee: iss.assignee,
113
- createdAt: iss.createdAt,
114
- description: iss.description,
115
- criteria: iss.criteria,
116
- },
117
- });
118
- nodeIds.add(id);
119
-
120
- // Edge: issue → its branch
121
- if (iss.branchName) {
122
- const branchId = `branch:${iss.branchName}`;
123
- if (nodeIds.has(branchId)) {
124
- edges.push({
125
- source: id,
126
- target: branchId,
127
- type: 'issue_branch',
128
- });
129
- }
130
- }
131
- }
132
-
133
- // --- Branches ---
134
- const branches = engine.listBranches();
135
- for (const b of branches) {
136
- const id = `branch:${b.name}`;
137
- if (!nodeIds.has(id)) {
138
- nodes.push({
139
- id,
140
- label: b.name,
141
- type: 'branch',
142
- meta: {
143
- isCurrent: b.isCurrent,
144
- createdAt: b.createdAt,
145
- },
146
- });
147
- nodeIds.add(id);
148
- }
149
-
150
- // Link issues that reference this branch
151
- for (const iss of issues) {
152
- if (iss.branchName === b.name) {
153
- edges.push({
154
- source: `issue:${iss.id}`,
155
- target: id,
156
- type: 'issue_branch',
157
- });
158
- }
159
- }
160
- }
161
-
162
- // --- Wiki-links (if markdown files exist) ---
163
- try {
164
- const mdFiles: Array<{ path: string; content: string }> = [];
165
- for (const f of files) {
166
- if (f.path.endsWith('.md')) {
167
- const absPath = join(engine.getRootPath(), f.path);
168
- if (existsSync(absPath)) {
169
- mdFiles.push({
170
- path: f.path,
171
- content: readFileSync(absPath, 'utf-8'),
172
- });
173
- }
174
- }
175
- }
176
-
177
- if (mdFiles.length > 0) {
178
- const ctx = createResolverContext({
179
- trackedFiles: () => files,
180
- listIssues: () => issues as any,
181
- listMilestones: () => milestones as any,
182
- } as any);
183
-
184
- const refIndex = buildRefIndex(mdFiles, ctx);
185
-
186
- for (const [filePath, refs] of refIndex.outgoing) {
187
- const sourceId = `file:${filePath}`;
188
- if (!nodeIds.has(sourceId)) continue;
189
- for (const ref of refs) {
190
- const targetId = `${ref.namespace}:${ref.target}`;
191
- // Normalize to our node IDs
192
- const candidateIds = [
193
- targetId,
194
- `file:${ref.target}`,
195
- `issue:${ref.target}`,
196
- `milestone:${ref.target}`,
197
- ];
198
- for (const cid of candidateIds) {
199
- if (nodeIds.has(cid) && cid !== sourceId) {
200
- edges.push({
201
- source: sourceId,
202
- target: cid,
203
- type: 'wikilink',
204
- label: ref.target,
205
- });
206
- break;
207
- }
208
- }
209
- }
210
- }
211
- }
212
- } catch {
213
- // Wiki-link indexing is best-effort
214
- }
215
-
216
- return { nodes, edges };
217
- }
218
-
219
- // ---------------------------------------------------------------------------
220
- // Node detail
221
- // ---------------------------------------------------------------------------
222
-
223
- function getNodeDetail(
224
- engine: TrellisVcsEngine,
225
- nodeId: string,
226
- ): Record<string, unknown> | null {
227
- const [type, ...rest] = nodeId.split(':');
228
- const id = rest.join(':');
229
-
230
- switch (type) {
231
- case 'file': {
232
- const files = engine.trackedFiles();
233
- const file = files.find((f) => f.path === id);
234
- if (!file) return null;
235
- // Get recent ops for this file
236
- const ops = engine.log({ filePath: id, limit: 10 });
237
- return {
238
- type: 'file',
239
- path: id,
240
- contentHash: file.contentHash,
241
- recentOps: ops.map((o) => ({
242
- kind: o.kind,
243
- timestamp: o.timestamp,
244
- hash: o.hash.slice(0, 24),
245
- })),
246
- };
247
- }
248
- case 'milestone': {
249
- const milestones = engine.listMilestones();
250
- const m = milestones.find((ms) => ms.id === id);
251
- if (!m) return null;
252
- return { type: 'milestone', ...m };
253
- }
254
- case 'issue': {
255
- const issue = engine.getIssue(id);
256
- if (!issue) return null;
257
- return { type: 'issue', ...issue };
258
- }
259
- case 'branch': {
260
- const branches = engine.listBranches();
261
- const b = branches.find((br) => br.name === id);
262
- if (!b) return null;
263
- return { type: 'branch', ...b };
264
- }
265
- default:
266
- return null;
267
- }
268
- }
269
-
270
- // ---------------------------------------------------------------------------
271
- // Server
272
- // ---------------------------------------------------------------------------
273
-
274
- export interface UIServerOptions {
275
- rootPath: string;
276
- port?: number;
277
- open?: boolean;
278
- }
279
-
280
- export async function startUIServer(opts: UIServerOptions): Promise<{
281
- port: number;
282
- stop: () => void;
283
- }> {
284
- const engine = new TrellisVcsEngine({ rootPath: opts.rootPath });
285
- engine.open();
286
-
287
- const clientHtml = readFileSync(
288
- join(dirname(new URL(import.meta.url).pathname), 'client.html'),
289
- 'utf-8',
290
- );
291
-
292
- // Lazy-load embedding manager for search
293
- let embeddingManager: any = null;
294
- function getEmbeddingManager() {
295
- if (!embeddingManager) {
296
- try {
297
- const { EmbeddingManager } = require('../embeddings/index.js');
298
- const dbPath = join(opts.rootPath, '.trellis', 'embeddings.db');
299
- if (existsSync(dbPath)) {
300
- embeddingManager = new EmbeddingManager(dbPath);
301
- }
302
- } catch {
303
- // Embeddings not available
304
- }
305
- }
306
- return embeddingManager;
307
- }
308
-
309
- const requestedPort = opts.port ?? 3333;
310
-
311
- const server = Bun.serve({
312
- port: requestedPort,
313
- async fetch(req) {
314
- const url = new URL(req.url);
315
- const path = url.pathname;
316
-
317
- // CORS headers
318
- const headers = {
319
- 'Access-Control-Allow-Origin': '*',
320
- 'Access-Control-Allow-Methods': 'GET, OPTIONS',
321
- 'Access-Control-Allow-Headers': 'Content-Type',
322
- };
323
-
324
- if (req.method === 'OPTIONS') {
325
- return new Response(null, { status: 204, headers });
326
- }
327
-
328
- // --- API Routes ---
329
-
330
- if (path === '/api/graph') {
331
- const graph = buildGraph(engine);
332
- return Response.json(graph, { headers });
333
- }
334
-
335
- if (path === '/api/search') {
336
- const query = url.searchParams.get('q');
337
- if (!query) {
338
- return Response.json(
339
- { error: 'Missing ?q= parameter' },
340
- { status: 400, headers },
341
- );
342
- }
343
- const limit = parseInt(url.searchParams.get('limit') ?? '10', 10);
344
- const typeFilter = url.searchParams.get('type');
345
-
346
- const mgr = getEmbeddingManager();
347
- if (!mgr) {
348
- return Response.json(
349
- {
350
- results: [],
351
- message: 'No embedding index. Run `trellis reindex` first.',
352
- },
353
- { headers },
354
- );
355
- }
356
-
357
- try {
358
- const searchOpts: any = { limit };
359
- if (typeFilter) {
360
- searchOpts.types = typeFilter
361
- .split(',')
362
- .map((t: string) => t.trim());
363
- }
364
- const results = await mgr.search(query, searchOpts);
365
- return Response.json(
366
- {
367
- results: results.map((r: any) => ({
368
- score: r.score,
369
- chunkType: r.chunk.chunkType,
370
- filePath: r.chunk.filePath,
371
- entityId: r.chunk.entityId,
372
- content: r.chunk.content,
373
- })),
374
- },
375
- { headers },
376
- );
377
- } catch (err: any) {
378
- return Response.json(
379
- { error: err.message },
380
- { status: 500, headers },
381
- );
382
- }
383
- }
384
-
385
- if (path.startsWith('/api/node/')) {
386
- const nodeId = decodeURIComponent(path.slice('/api/node/'.length));
387
- const detail = getNodeDetail(engine, nodeId);
388
- if (!detail) {
389
- return Response.json(
390
- { error: 'Node not found' },
391
- { status: 404, headers },
392
- );
393
- }
394
- return Response.json(detail, { headers });
395
- }
396
-
397
- // --- Static ---
398
- if (path === '/' || path === '/index.html') {
399
- return new Response(clientHtml, {
400
- headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8' },
401
- });
402
- }
403
-
404
- return new Response('Not Found', { status: 404, headers });
405
- },
406
- });
407
-
408
- return {
409
- port: server.port as number,
410
- stop: () => {
411
- server.stop();
412
- if (embeddingManager) {
413
- try {
414
- embeddingManager.close();
415
- } catch {}
416
- }
417
- },
418
- };
419
- }
@@ -1,124 +0,0 @@
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
- }
package/src/vcs/branch.ts DELETED
@@ -1,150 +0,0 @@
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
- }