trellis 1.0.8 → 2.0.6

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 +564 -83
  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,1556 @@
1
+ // @bun
2
+ import {
3
+ createVcsOp,
4
+ criterionEntityId,
5
+ decisionEntityId,
6
+ dirEntityId,
7
+ fileEntityId,
8
+ issueEntityId
9
+ } from "./index-fd4e26s4.js";
10
+ import {
11
+ __require
12
+ } from "./index-a76rekgs.js";
13
+
14
+ // src/vcs/decompose.ts
15
+ import { dirname } from "path";
16
+ function decompose(op) {
17
+ const result = {
18
+ addFacts: [],
19
+ addLinks: [],
20
+ deleteFacts: [],
21
+ deleteLinks: []
22
+ };
23
+ const vcs = op.vcs;
24
+ if (!vcs)
25
+ return result;
26
+ switch (op.kind) {
27
+ case "vcs:fileAdd": {
28
+ if (!vcs.filePath)
29
+ break;
30
+ const eid = fileEntityId(vcs.filePath);
31
+ const dir = dirname(vcs.filePath);
32
+ const did = dirEntityId(dir === "." ? "" : dir);
33
+ result.addFacts.push({ e: eid, a: "type", v: "FileNode" }, { e: eid, a: "path", v: vcs.filePath });
34
+ if (vcs.contentHash) {
35
+ result.addFacts.push({ e: eid, a: "contentHash", v: vcs.contentHash });
36
+ }
37
+ if (vcs.size !== undefined) {
38
+ result.addFacts.push({ e: eid, a: "size", v: vcs.size });
39
+ }
40
+ if (vcs.language) {
41
+ result.addFacts.push({ e: eid, a: "language", v: vcs.language });
42
+ }
43
+ result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
44
+ result.addFacts.push({ e: did, a: "type", v: "DirectoryNode" }, { e: did, a: "path", v: dir === "." ? "" : dir });
45
+ result.addLinks.push({ e1: did, a: "contains", e2: eid });
46
+ break;
47
+ }
48
+ case "vcs:fileModify": {
49
+ if (!vcs.filePath)
50
+ break;
51
+ const eid = fileEntityId(vcs.filePath);
52
+ if (vcs.oldContentHash) {
53
+ result.deleteFacts.push({
54
+ e: eid,
55
+ a: "contentHash",
56
+ v: vcs.oldContentHash
57
+ });
58
+ }
59
+ if (vcs.contentHash) {
60
+ result.addFacts.push({ e: eid, a: "contentHash", v: vcs.contentHash });
61
+ }
62
+ if (vcs.size !== undefined) {
63
+ result.addFacts.push({ e: eid, a: "size", v: vcs.size });
64
+ }
65
+ result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
66
+ break;
67
+ }
68
+ case "vcs:fileDelete": {
69
+ if (!vcs.filePath)
70
+ break;
71
+ const eid = fileEntityId(vcs.filePath);
72
+ const dir = dirname(vcs.filePath);
73
+ const did = dirEntityId(dir === "." ? "" : dir);
74
+ result.deleteFacts.push({ e: eid, a: "type", v: "FileNode" }, { e: eid, a: "path", v: vcs.filePath });
75
+ if (vcs.contentHash) {
76
+ result.deleteFacts.push({
77
+ e: eid,
78
+ a: "contentHash",
79
+ v: vcs.contentHash
80
+ });
81
+ }
82
+ result.deleteLinks.push({ e1: did, a: "contains", e2: eid });
83
+ break;
84
+ }
85
+ case "vcs:fileRename": {
86
+ if (!vcs.filePath || !vcs.oldFilePath)
87
+ break;
88
+ const eid = fileEntityId(vcs.oldFilePath);
89
+ const oldDir = dirname(vcs.oldFilePath);
90
+ const newDir = dirname(vcs.filePath);
91
+ const oldDid = dirEntityId(oldDir === "." ? "" : oldDir);
92
+ const newDid = dirEntityId(newDir === "." ? "" : newDir);
93
+ result.deleteFacts.push({ e: eid, a: "path", v: vcs.oldFilePath });
94
+ result.addFacts.push({ e: eid, a: "path", v: vcs.filePath });
95
+ result.addFacts.push({ e: eid, a: "lastModified", v: op.timestamp });
96
+ result.deleteLinks.push({ e1: oldDid, a: "contains", e2: eid });
97
+ result.addFacts.push({ e: newDid, a: "type", v: "DirectoryNode" }, { e: newDid, a: "path", v: newDir === "." ? "" : newDir });
98
+ result.addLinks.push({ e1: newDid, a: "contains", e2: eid });
99
+ break;
100
+ }
101
+ case "vcs:branchCreate": {
102
+ if (!vcs.branchName)
103
+ break;
104
+ const bid = `branch:${vcs.branchName}`;
105
+ result.addFacts.push({ e: bid, a: "type", v: "Branch" }, { e: bid, a: "name", v: vcs.branchName }, { e: bid, a: "createdAt", v: op.timestamp }, { e: bid, a: "createdBy", v: op.agentId });
106
+ if (vcs.targetOpHash) {
107
+ result.addFacts.push({ e: bid, a: "headOpHash", v: vcs.targetOpHash });
108
+ }
109
+ if (vcs.baseBranch) {
110
+ result.addLinks.push({
111
+ e1: bid,
112
+ a: "forkedFrom",
113
+ e2: `branch:${vcs.baseBranch}`
114
+ });
115
+ }
116
+ break;
117
+ }
118
+ case "vcs:branchDelete": {
119
+ if (!vcs.branchName)
120
+ break;
121
+ const bid = `branch:${vcs.branchName}`;
122
+ result.deleteFacts.push({ e: bid, a: "type", v: "Branch" }, { e: bid, a: "name", v: vcs.branchName });
123
+ break;
124
+ }
125
+ case "vcs:branchAdvance": {
126
+ if (!vcs.branchName || !vcs.targetOpHash)
127
+ break;
128
+ const bid = `branch:${vcs.branchName}`;
129
+ result.addFacts.push({ e: bid, a: "headOpHash", v: vcs.targetOpHash });
130
+ break;
131
+ }
132
+ case "vcs:milestoneCreate": {
133
+ if (!vcs.milestoneId)
134
+ break;
135
+ const mid = vcs.milestoneId;
136
+ result.addFacts.push({ e: mid, a: "type", v: "Milestone" }, { e: mid, a: "createdAt", v: op.timestamp }, { e: mid, a: "createdBy", v: op.agentId });
137
+ if (vcs.message) {
138
+ result.addFacts.push({ e: mid, a: "message", v: vcs.message });
139
+ }
140
+ if (vcs.fromOpHash) {
141
+ result.addFacts.push({ e: mid, a: "fromOpHash", v: vcs.fromOpHash });
142
+ }
143
+ if (vcs.toOpHash) {
144
+ result.addFacts.push({ e: mid, a: "toOpHash", v: vcs.toOpHash });
145
+ }
146
+ break;
147
+ }
148
+ case "vcs:checkpointCreate": {
149
+ const cid = `checkpoint:${op.hash}`;
150
+ result.addFacts.push({ e: cid, a: "type", v: "Checkpoint" }, { e: cid, a: "createdAt", v: op.timestamp }, { e: cid, a: "atOpHash", v: op.hash });
151
+ if (vcs.trigger) {
152
+ result.addFacts.push({ e: cid, a: "trigger", v: vcs.trigger });
153
+ }
154
+ break;
155
+ }
156
+ case "vcs:issueCreate": {
157
+ if (!vcs.issueId)
158
+ break;
159
+ const eid = issueEntityId(vcs.issueId);
160
+ result.addFacts.push({ e: eid, a: "type", v: "Issue" }, { e: eid, a: "status", v: vcs.issueStatus ?? "backlog" }, { e: eid, a: "createdAt", v: op.timestamp }, { e: eid, a: "createdBy", v: op.agentId });
161
+ if (vcs.issueTitle) {
162
+ result.addFacts.push({ e: eid, a: "title", v: vcs.issueTitle });
163
+ }
164
+ if (vcs.issueDescription) {
165
+ result.addFacts.push({
166
+ e: eid,
167
+ a: "description",
168
+ v: vcs.issueDescription
169
+ });
170
+ }
171
+ if (vcs.issuePriority) {
172
+ result.addFacts.push({ e: eid, a: "priority", v: vcs.issuePriority });
173
+ }
174
+ if (vcs.issueLabels && vcs.issueLabels.length > 0) {
175
+ result.addFacts.push({
176
+ e: eid,
177
+ a: "labels",
178
+ v: vcs.issueLabels.join(",")
179
+ });
180
+ }
181
+ if (vcs.issueAssignee) {
182
+ result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
183
+ }
184
+ if (vcs.parentIssueId) {
185
+ result.addLinks.push({
186
+ e1: eid,
187
+ a: "childOf",
188
+ e2: issueEntityId(vcs.parentIssueId)
189
+ });
190
+ }
191
+ break;
192
+ }
193
+ case "vcs:issueUpdate": {
194
+ if (!vcs.issueId)
195
+ break;
196
+ const eid = issueEntityId(vcs.issueId);
197
+ if (vcs.issueStatus) {
198
+ result.addFacts.push({ e: eid, a: "status", v: vcs.issueStatus });
199
+ }
200
+ if (vcs.issuePriority) {
201
+ result.addFacts.push({ e: eid, a: "priority", v: vcs.issuePriority });
202
+ }
203
+ if (vcs.issueLabels) {
204
+ result.addFacts.push({
205
+ e: eid,
206
+ a: "labels",
207
+ v: vcs.issueLabels.join(",")
208
+ });
209
+ }
210
+ if (vcs.issueTitle) {
211
+ result.addFacts.push({ e: eid, a: "title", v: vcs.issueTitle });
212
+ }
213
+ if (vcs.issueAssignee) {
214
+ result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
215
+ }
216
+ if (vcs.issueDescription !== undefined) {
217
+ result.addFacts.push({
218
+ e: eid,
219
+ a: "description",
220
+ v: vcs.issueDescription
221
+ });
222
+ }
223
+ break;
224
+ }
225
+ case "vcs:issueStart": {
226
+ if (!vcs.issueId)
227
+ break;
228
+ const eid = issueEntityId(vcs.issueId);
229
+ result.addFacts.push({ e: eid, a: "status", v: "in_progress" }, { e: eid, a: "startedAt", v: op.timestamp });
230
+ if (vcs.issueAssignee) {
231
+ result.addFacts.push({ e: eid, a: "assignee", v: vcs.issueAssignee });
232
+ }
233
+ if (vcs.branchName) {
234
+ result.addLinks.push({
235
+ e1: eid,
236
+ a: "trackedOn",
237
+ e2: `branch:${vcs.branchName}`
238
+ });
239
+ }
240
+ break;
241
+ }
242
+ case "vcs:issuePause": {
243
+ if (!vcs.issueId)
244
+ break;
245
+ const eid = issueEntityId(vcs.issueId);
246
+ result.addFacts.push({ e: eid, a: "status", v: "paused" }, { e: eid, a: "pausedAt", v: op.timestamp });
247
+ if (vcs.pauseNote) {
248
+ result.addFacts.push({ e: eid, a: "pauseNote", v: vcs.pauseNote });
249
+ }
250
+ break;
251
+ }
252
+ case "vcs:issueResume": {
253
+ if (!vcs.issueId)
254
+ break;
255
+ const eid = issueEntityId(vcs.issueId);
256
+ result.addFacts.push({ e: eid, a: "status", v: "in_progress" }, { e: eid, a: "resumedAt", v: op.timestamp }, { e: eid, a: "pauseNote", v: "" });
257
+ break;
258
+ }
259
+ case "vcs:issueClose": {
260
+ if (!vcs.issueId)
261
+ break;
262
+ const eid = issueEntityId(vcs.issueId);
263
+ result.addFacts.push({ e: eid, a: "status", v: "closed" }, { e: eid, a: "closedAt", v: op.timestamp });
264
+ break;
265
+ }
266
+ case "vcs:issueReopen": {
267
+ if (!vcs.issueId)
268
+ break;
269
+ const eid = issueEntityId(vcs.issueId);
270
+ result.addFacts.push({ e: eid, a: "status", v: "queue" });
271
+ break;
272
+ }
273
+ case "vcs:criterionAdd": {
274
+ if (!vcs.criterionId || !vcs.issueId)
275
+ break;
276
+ const ceid = vcs.criterionId;
277
+ result.addFacts.push({ e: ceid, a: "type", v: "Criterion" }, { e: ceid, a: "status", v: "pending" }, { e: ceid, a: "createdAt", v: op.timestamp });
278
+ if (vcs.criterionDescription) {
279
+ result.addFacts.push({
280
+ e: ceid,
281
+ a: "description",
282
+ v: vcs.criterionDescription
283
+ });
284
+ }
285
+ if (vcs.criterionCommand) {
286
+ result.addFacts.push({
287
+ e: ceid,
288
+ a: "command",
289
+ v: vcs.criterionCommand
290
+ });
291
+ }
292
+ result.addLinks.push({
293
+ e1: ceid,
294
+ a: "criterionOf",
295
+ e2: issueEntityId(vcs.issueId)
296
+ });
297
+ break;
298
+ }
299
+ case "vcs:criterionUpdate": {
300
+ if (!vcs.criterionId)
301
+ break;
302
+ const ceid = vcs.criterionId;
303
+ if (vcs.criterionStatus) {
304
+ result.addFacts.push({ e: ceid, a: "status", v: vcs.criterionStatus });
305
+ }
306
+ if (vcs.criterionOutput) {
307
+ result.addFacts.push({
308
+ e: ceid,
309
+ a: "lastOutput",
310
+ v: vcs.criterionOutput
311
+ });
312
+ }
313
+ result.addFacts.push({ e: ceid, a: "lastRunAt", v: op.timestamp });
314
+ break;
315
+ }
316
+ case "vcs:issueBlock": {
317
+ if (!vcs.issueId || !vcs.blockedByIssueId)
318
+ break;
319
+ const eid = issueEntityId(vcs.issueId);
320
+ const blockerEid = issueEntityId(vcs.blockedByIssueId);
321
+ result.addLinks.push({ e1: eid, a: "blockedBy", e2: blockerEid });
322
+ break;
323
+ }
324
+ case "vcs:issueUnblock": {
325
+ if (!vcs.issueId || !vcs.blockedByIssueId)
326
+ break;
327
+ const eid = issueEntityId(vcs.issueId);
328
+ const blockerEid = issueEntityId(vcs.blockedByIssueId);
329
+ result.deleteLinks.push({ e1: eid, a: "blockedBy", e2: blockerEid });
330
+ break;
331
+ }
332
+ case "vcs:decisionRecord": {
333
+ if (!vcs.decisionId)
334
+ break;
335
+ const did = decisionEntityId(vcs.decisionId);
336
+ result.addFacts.push({ e: did, a: "type", v: "Decision" }, { e: did, a: "createdAt", v: op.timestamp }, { e: did, a: "createdBy", v: op.agentId });
337
+ if (vcs.decisionToolName) {
338
+ result.addFacts.push({
339
+ e: did,
340
+ a: "toolName",
341
+ v: vcs.decisionToolName
342
+ });
343
+ }
344
+ if (vcs.decisionToolInput) {
345
+ result.addFacts.push({
346
+ e: did,
347
+ a: "toolInput",
348
+ v: vcs.decisionToolInput
349
+ });
350
+ }
351
+ if (vcs.decisionToolOutput) {
352
+ result.addFacts.push({
353
+ e: did,
354
+ a: "outputSummary",
355
+ v: vcs.decisionToolOutput
356
+ });
357
+ }
358
+ if (vcs.decisionContext) {
359
+ result.addFacts.push({ e: did, a: "context", v: vcs.decisionContext });
360
+ }
361
+ if (vcs.decisionRationale) {
362
+ result.addFacts.push({
363
+ e: did,
364
+ a: "rationale",
365
+ v: vcs.decisionRationale
366
+ });
367
+ }
368
+ if (vcs.decisionAlternatives) {
369
+ result.addFacts.push({
370
+ e: did,
371
+ a: "alternatives",
372
+ v: vcs.decisionAlternatives
373
+ });
374
+ }
375
+ break;
376
+ }
377
+ }
378
+ return result;
379
+ }
380
+
381
+ // src/vcs/blob-store.ts
382
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
383
+ import { join } from "path";
384
+
385
+ class BlobStore {
386
+ blobDir;
387
+ constructor(trellisDir) {
388
+ this.blobDir = join(trellisDir, "blobs");
389
+ if (!existsSync(this.blobDir)) {
390
+ mkdirSync(this.blobDir, { recursive: true });
391
+ }
392
+ }
393
+ async put(content) {
394
+ const hash = await this.hash(content);
395
+ const blobPath = join(this.blobDir, hash);
396
+ if (!existsSync(blobPath)) {
397
+ writeFileSync(blobPath, content);
398
+ }
399
+ return hash;
400
+ }
401
+ putSync(content) {
402
+ const hash = this.hashSync(content);
403
+ const blobPath = join(this.blobDir, hash);
404
+ if (!existsSync(blobPath)) {
405
+ writeFileSync(blobPath, content);
406
+ }
407
+ return hash;
408
+ }
409
+ get(hash) {
410
+ const blobPath = join(this.blobDir, hash);
411
+ if (!existsSync(blobPath)) {
412
+ return null;
413
+ }
414
+ return readFileSync(blobPath);
415
+ }
416
+ has(hash) {
417
+ return existsSync(join(this.blobDir, hash));
418
+ }
419
+ async hash(content) {
420
+ const hashBuffer = await crypto.subtle.digest("SHA-256", content);
421
+ return this.hexFromBuffer(hashBuffer);
422
+ }
423
+ hashSync(content) {
424
+ const hasher = new Bun.CryptoHasher("sha256");
425
+ hasher.update(content);
426
+ return hasher.digest("hex");
427
+ }
428
+ count() {
429
+ try {
430
+ const { readdirSync } = __require("fs");
431
+ return readdirSync(this.blobDir).length;
432
+ } catch {
433
+ return 0;
434
+ }
435
+ }
436
+ totalSize() {
437
+ try {
438
+ const { readdirSync, statSync } = __require("fs");
439
+ const files = readdirSync(this.blobDir);
440
+ return files.reduce((sum, f) => {
441
+ try {
442
+ return sum + statSync(join(this.blobDir, f)).size;
443
+ } catch {
444
+ return sum;
445
+ }
446
+ }, 0);
447
+ } catch {
448
+ return 0;
449
+ }
450
+ }
451
+ hexFromBuffer(buffer) {
452
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
453
+ }
454
+ }
455
+
456
+ // src/vcs/branch.ts
457
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
458
+ import { join as join2 } from "path";
459
+ async function createBranch(ctx, name, currentBranch) {
460
+ const existing = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Branch" && f.e === `branch:${name}`);
461
+ if (existing.length > 0) {
462
+ throw new Error(`Branch '${name}' already exists`);
463
+ }
464
+ const op = await createVcsOp("vcs:branchCreate", {
465
+ agentId: ctx.agentId,
466
+ previousHash: ctx.getLastOp()?.hash,
467
+ vcs: {
468
+ branchName: name,
469
+ baseBranch: currentBranch,
470
+ targetOpHash: ctx.getLastOp()?.hash
471
+ }
472
+ });
473
+ ctx.applyOp(op);
474
+ return op;
475
+ }
476
+ function switchBranch(ctx, name) {
477
+ const branchFacts = ctx.store.getFactsByEntity(`branch:${name}`).filter((f) => f.a === "type" && f.v === "Branch");
478
+ if (branchFacts.length === 0) {
479
+ throw new Error(`Branch '${name}' does not exist`);
480
+ }
481
+ }
482
+ function listBranches(ctx, currentBranch) {
483
+ const branchFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Branch");
484
+ return branchFacts.map((f) => {
485
+ const nameFact = ctx.store.getFactsByEntity(f.e).find((ef) => ef.a === "name");
486
+ const createdFact = ctx.store.getFactsByEntity(f.e).find((ef) => ef.a === "createdAt");
487
+ const name = nameFact?.v ?? f.e.replace("branch:", "");
488
+ return {
489
+ name,
490
+ isCurrent: name === currentBranch,
491
+ createdAt: createdFact?.v
492
+ };
493
+ });
494
+ }
495
+ async function deleteBranch(ctx, name, currentBranch) {
496
+ if (name === currentBranch) {
497
+ throw new Error(`Cannot delete the current branch '${name}'`);
498
+ }
499
+ const branchFacts = ctx.store.getFactsByEntity(`branch:${name}`).filter((f) => f.a === "type" && f.v === "Branch");
500
+ if (branchFacts.length === 0) {
501
+ throw new Error(`Branch '${name}' does not exist`);
502
+ }
503
+ const op = await createVcsOp("vcs:branchDelete", {
504
+ agentId: ctx.agentId,
505
+ previousHash: ctx.getLastOp()?.hash,
506
+ vcs: { branchName: name }
507
+ });
508
+ ctx.applyOp(op);
509
+ return op;
510
+ }
511
+ function saveBranchState(rootPath, state) {
512
+ const statePath = join2(rootPath, ".trellis", "state.json");
513
+ writeFileSync2(statePath, JSON.stringify(state));
514
+ }
515
+ function loadBranchState(rootPath) {
516
+ const statePath = join2(rootPath, ".trellis", "state.json");
517
+ if (existsSync2(statePath)) {
518
+ try {
519
+ const raw = readFileSync2(statePath, "utf-8");
520
+ const state = JSON.parse(raw);
521
+ if (state.currentBranch) {
522
+ return { currentBranch: state.currentBranch };
523
+ }
524
+ } catch {}
525
+ }
526
+ return { currentBranch: "main" };
527
+ }
528
+
529
+ // src/vcs/milestone.ts
530
+ async function createMilestone(ctx, message, opts) {
531
+ const ops = ctx.readAllOps();
532
+ const toOpHash = opts?.toOpHash ?? ops[ops.length - 1]?.hash;
533
+ let fromOpHash = opts?.fromOpHash;
534
+ if (!fromOpHash) {
535
+ const milestones = ops.filter((o) => o.kind === "vcs:milestoneCreate");
536
+ if (milestones.length > 0) {
537
+ const lastMilestone = milestones[milestones.length - 1];
538
+ fromOpHash = lastMilestone.vcs?.toOpHash ?? lastMilestone.hash;
539
+ } else {
540
+ fromOpHash = ops[0]?.hash;
541
+ }
542
+ }
543
+ const idBase = `${message}:${Date.now()}`;
544
+ const msgUint8 = new TextEncoder().encode(idBase);
545
+ const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8);
546
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
547
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
548
+ const milestoneId = `milestone:${hashHex.slice(0, 12)}`;
549
+ const fromIdx = ops.findIndex((o) => o.hash === fromOpHash);
550
+ const toIdx = ops.findIndex((o) => o.hash === toOpHash);
551
+ const rangeOps = fromIdx >= 0 && toIdx >= 0 ? ops.slice(fromIdx, toIdx + 1) : ops;
552
+ const affectedFiles = [
553
+ ...new Set(rangeOps.filter((o) => o.vcs?.filePath).map((o) => o.vcs.filePath))
554
+ ];
555
+ const op = await createVcsOp("vcs:milestoneCreate", {
556
+ agentId: ctx.agentId,
557
+ previousHash: ctx.getLastOp()?.hash,
558
+ vcs: {
559
+ milestoneId,
560
+ message,
561
+ fromOpHash,
562
+ toOpHash
563
+ }
564
+ });
565
+ ctx.applyOp(op);
566
+ for (const file of affectedFiles) {
567
+ ctx.store.addFacts([{ e: milestoneId, a: "affectsFile", v: file }]);
568
+ }
569
+ return op;
570
+ }
571
+ function listMilestones(ctx) {
572
+ const milestoneFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Milestone");
573
+ return milestoneFacts.map((f) => {
574
+ const facts = ctx.store.getFactsByEntity(f.e);
575
+ const get = (attr) => facts.find((ef) => ef.a === attr)?.v;
576
+ const affectedFiles = facts.filter((ef) => ef.a === "affectsFile").map((ef) => ef.v);
577
+ return {
578
+ id: f.e,
579
+ message: get("message"),
580
+ createdAt: get("createdAt"),
581
+ createdBy: get("createdBy"),
582
+ fromOpHash: get("fromOpHash"),
583
+ toOpHash: get("toOpHash"),
584
+ affectedFiles
585
+ };
586
+ });
587
+ }
588
+
589
+ // src/vcs/checkpoint.ts
590
+ async function createCheckpoint(ctx, trigger = "manual") {
591
+ const op = await createVcsOp("vcs:checkpointCreate", {
592
+ agentId: ctx.agentId,
593
+ previousHash: ctx.getLastOp()?.hash,
594
+ vcs: { trigger }
595
+ });
596
+ ctx.applyOp(op);
597
+ return op;
598
+ }
599
+ function listCheckpoints(ctx) {
600
+ const cpFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Checkpoint");
601
+ return cpFacts.map((f) => {
602
+ const facts = ctx.store.getFactsByEntity(f.e);
603
+ const get = (attr) => facts.find((ef) => ef.a === attr)?.v;
604
+ return {
605
+ id: f.e,
606
+ createdAt: get("createdAt"),
607
+ trigger: get("trigger"),
608
+ atOpHash: get("atOpHash")
609
+ };
610
+ });
611
+ }
612
+
613
+ // src/vcs/diff.ts
614
+ function diffFileStates(stateA, stateB, blobStore) {
615
+ const diffs = [];
616
+ for (const [path, bState] of stateB) {
617
+ if (bState.deleted)
618
+ continue;
619
+ const aState = stateA.get(path);
620
+ if (!aState || aState.deleted) {
621
+ diffs.push({
622
+ kind: "fileAdded",
623
+ path,
624
+ newContentHash: bState.contentHash
625
+ });
626
+ } else if (aState.contentHash !== bState.contentHash) {
627
+ const diff = {
628
+ kind: "fileModified",
629
+ path,
630
+ oldContentHash: aState.contentHash,
631
+ newContentHash: bState.contentHash
632
+ };
633
+ if (blobStore && aState.contentHash && bState.contentHash) {
634
+ const oldContent = blobStore.get(aState.contentHash);
635
+ const newContent = blobStore.get(bState.contentHash);
636
+ if (oldContent && newContent) {
637
+ diff.unifiedDiff = generateUnifiedDiff(path, oldContent.toString("utf-8"), newContent.toString("utf-8"));
638
+ }
639
+ }
640
+ diffs.push(diff);
641
+ }
642
+ }
643
+ for (const [path, aState] of stateA) {
644
+ if (aState.deleted)
645
+ continue;
646
+ const bState = stateB.get(path);
647
+ if (!bState || bState.deleted) {
648
+ diffs.push({
649
+ kind: "fileDeleted",
650
+ path,
651
+ oldContentHash: aState.contentHash
652
+ });
653
+ }
654
+ }
655
+ const stats = {
656
+ added: diffs.filter((d) => d.kind === "fileAdded").length,
657
+ modified: diffs.filter((d) => d.kind === "fileModified").length,
658
+ removed: diffs.filter((d) => d.kind === "fileDeleted").length,
659
+ renamed: diffs.filter((d) => d.kind === "fileRenamed").length
660
+ };
661
+ return {
662
+ diffs,
663
+ filesChanged: diffs.map((d) => d.path),
664
+ stats
665
+ };
666
+ }
667
+ function buildFileStateAtOp(ops, atOpHash) {
668
+ const state = new Map;
669
+ for (const op of ops) {
670
+ if (op.vcs?.filePath) {
671
+ switch (op.kind) {
672
+ case "vcs:fileAdd":
673
+ case "vcs:fileModify":
674
+ state.set(op.vcs.filePath, { contentHash: op.vcs.contentHash });
675
+ break;
676
+ case "vcs:fileDelete":
677
+ state.set(op.vcs.filePath, { deleted: true });
678
+ break;
679
+ case "vcs:fileRename":
680
+ if (op.vcs.oldFilePath) {
681
+ state.set(op.vcs.oldFilePath, { deleted: true });
682
+ }
683
+ state.set(op.vcs.filePath, { contentHash: op.vcs.contentHash });
684
+ break;
685
+ }
686
+ }
687
+ if (atOpHash && op.hash === atOpHash)
688
+ break;
689
+ }
690
+ return state;
691
+ }
692
+ function diffOpRange(ops, fromHash, toHash, blobStore) {
693
+ const stateA = buildFileStateAtOp(ops, fromHash);
694
+ const stateB = buildFileStateAtOp(ops, toHash);
695
+ return diffFileStates(stateA, stateB, blobStore);
696
+ }
697
+ function generateUnifiedDiff(filePath, oldText, newText, contextLines = 3) {
698
+ const oldLines = oldText.split(`
699
+ `);
700
+ const newLines = newText.split(`
701
+ `);
702
+ const edits = myersDiff(oldLines, newLines);
703
+ const hunks = buildHunks(edits, contextLines);
704
+ if (hunks.length === 0)
705
+ return "";
706
+ const lines = [`--- a/${filePath}`, `+++ b/${filePath}`];
707
+ for (const hunk of hunks) {
708
+ lines.push(`@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`);
709
+ for (const edit of hunk.edits) {
710
+ switch (edit.kind) {
711
+ case "equal":
712
+ lines.push(` ${edit.line}`);
713
+ break;
714
+ case "delete":
715
+ lines.push(`-${edit.line}`);
716
+ break;
717
+ case "insert":
718
+ lines.push(`+${edit.line}`);
719
+ break;
720
+ }
721
+ }
722
+ }
723
+ return lines.join(`
724
+ `);
725
+ }
726
+ function myersDiff(oldLines, newLines) {
727
+ const n = oldLines.length;
728
+ const m = newLines.length;
729
+ if (n === 0 && m === 0)
730
+ return [];
731
+ if (n === 0)
732
+ return newLines.map((line) => ({ kind: "insert", line }));
733
+ if (m === 0)
734
+ return oldLines.map((line) => ({ kind: "delete", line }));
735
+ const max = n + m;
736
+ const size = 2 * max + 1;
737
+ const v = new Int32Array(size);
738
+ const trace = [];
739
+ const off = max;
740
+ outer:
741
+ for (let d = 0;d <= max; d++) {
742
+ trace.push(v.slice());
743
+ for (let k = -d;k <= d; k += 2) {
744
+ let x2;
745
+ if (k === -d || k !== d && v[k - 1 + off] < v[k + 1 + off]) {
746
+ x2 = v[k + 1 + off];
747
+ } else {
748
+ x2 = v[k - 1 + off] + 1;
749
+ }
750
+ let y2 = x2 - k;
751
+ while (x2 < n && y2 < m && oldLines[x2] === newLines[y2]) {
752
+ x2++;
753
+ y2++;
754
+ }
755
+ v[k + off] = x2;
756
+ if (x2 >= n && y2 >= m)
757
+ break outer;
758
+ }
759
+ }
760
+ let x = n;
761
+ let y = m;
762
+ const edits = [];
763
+ for (let d = trace.length - 1;d >= 0; d--) {
764
+ const tv = trace[d];
765
+ const k = x - y;
766
+ let prevK;
767
+ if (d === 0) {
768
+ while (x > 0 && y > 0) {
769
+ x--;
770
+ y--;
771
+ edits.push({ kind: "equal", line: oldLines[x] });
772
+ }
773
+ break;
774
+ }
775
+ if (k === -d || k !== d && tv[k - 1 + off] < tv[k + 1 + off]) {
776
+ prevK = k + 1;
777
+ } else {
778
+ prevK = k - 1;
779
+ }
780
+ const prevX = tv[prevK + off];
781
+ const prevY = prevX - prevK;
782
+ while (x > prevX && y > prevY) {
783
+ x--;
784
+ y--;
785
+ edits.push({ kind: "equal", line: oldLines[x] });
786
+ }
787
+ if (prevK === k + 1) {
788
+ y--;
789
+ edits.push({ kind: "insert", line: newLines[y] });
790
+ } else {
791
+ x--;
792
+ edits.push({ kind: "delete", line: oldLines[x] });
793
+ }
794
+ }
795
+ edits.reverse();
796
+ return edits;
797
+ }
798
+ function buildHunks(edits, contextLines) {
799
+ if (edits.length === 0)
800
+ return [];
801
+ const changeIndices = [];
802
+ for (let i = 0;i < edits.length; i++) {
803
+ if (edits[i].kind !== "equal") {
804
+ changeIndices.push(i);
805
+ }
806
+ }
807
+ if (changeIndices.length === 0)
808
+ return [];
809
+ const hunks = [];
810
+ let hunkStart = Math.max(0, changeIndices[0] - contextLines);
811
+ let hunkEnd = Math.min(edits.length - 1, changeIndices[0] + contextLines);
812
+ for (let i = 1;i < changeIndices.length; i++) {
813
+ const changeStart = changeIndices[i] - contextLines;
814
+ const changeEnd = Math.min(edits.length - 1, changeIndices[i] + contextLines);
815
+ if (changeStart <= hunkEnd + 1) {
816
+ hunkEnd = changeEnd;
817
+ } else {
818
+ hunks.push(createHunk(edits, hunkStart, hunkEnd));
819
+ hunkStart = changeStart;
820
+ hunkEnd = changeEnd;
821
+ }
822
+ }
823
+ hunks.push(createHunk(edits, hunkStart, hunkEnd));
824
+ return hunks;
825
+ }
826
+ function createHunk(edits, start, end) {
827
+ const hunkEdits = edits.slice(start, end + 1);
828
+ let oldLine = 1;
829
+ let newLine = 1;
830
+ for (let i = 0;i < start; i++) {
831
+ if (edits[i].kind === "equal" || edits[i].kind === "delete")
832
+ oldLine++;
833
+ if (edits[i].kind === "equal" || edits[i].kind === "insert")
834
+ newLine++;
835
+ }
836
+ let oldCount = 0;
837
+ let newCount = 0;
838
+ for (const edit of hunkEdits) {
839
+ if (edit.kind === "equal" || edit.kind === "delete")
840
+ oldCount++;
841
+ if (edit.kind === "equal" || edit.kind === "insert")
842
+ newCount++;
843
+ }
844
+ return {
845
+ oldStart: oldLine,
846
+ oldCount,
847
+ newStart: newLine,
848
+ newCount,
849
+ edits: hunkEdits
850
+ };
851
+ }
852
+
853
+ // src/vcs/merge.ts
854
+ function threeWayMerge(base, ours, theirs, blobStore) {
855
+ const mergedFiles = new Map;
856
+ const conflicts = [];
857
+ const allPaths = new Set;
858
+ for (const [p, s] of base)
859
+ if (!s.deleted)
860
+ allPaths.add(p);
861
+ for (const [p, s] of ours)
862
+ if (!s.deleted)
863
+ allPaths.add(p);
864
+ for (const [p, s] of theirs)
865
+ if (!s.deleted)
866
+ allPaths.add(p);
867
+ for (const [p, s] of ours)
868
+ if (s.deleted)
869
+ allPaths.add(p);
870
+ for (const [p, s] of theirs)
871
+ if (s.deleted)
872
+ allPaths.add(p);
873
+ for (const path of allPaths) {
874
+ const b = base.get(path);
875
+ const o = ours.get(path);
876
+ const t = theirs.get(path);
877
+ const baseExists = b && !b.deleted;
878
+ const oursExists = o && !o.deleted;
879
+ const theirsExists = t && !t.deleted;
880
+ const baseHash = baseExists ? b.contentHash : undefined;
881
+ const oursHash = oursExists ? o.contentHash : undefined;
882
+ const theirsHash = theirsExists ? t.contentHash : undefined;
883
+ if (oursHash === theirsHash) {
884
+ continue;
885
+ }
886
+ if (theirsHash === baseHash && oursHash !== baseHash) {
887
+ if (!oursExists) {
888
+ mergedFiles.set(path, null);
889
+ }
890
+ continue;
891
+ }
892
+ if (oursHash === baseHash && theirsHash !== baseHash) {
893
+ if (!theirsExists) {
894
+ mergedFiles.set(path, null);
895
+ } else if (theirsHash && blobStore) {
896
+ const content = blobStore.get(theirsHash);
897
+ if (content) {
898
+ mergedFiles.set(path, content.toString("utf-8"));
899
+ }
900
+ }
901
+ continue;
902
+ }
903
+ if (!baseExists && oursExists && theirsExists) {
904
+ if (oursHash === theirsHash) {
905
+ continue;
906
+ }
907
+ const oursContent = oursHash && blobStore ? blobStore.get(oursHash)?.toString("utf-8") : undefined;
908
+ const theirsContent = theirsHash && blobStore ? blobStore.get(theirsHash)?.toString("utf-8") : undefined;
909
+ if (oursContent !== undefined && theirsContent !== undefined) {
910
+ const textResult = threeWayTextMerge("", oursContent, theirsContent);
911
+ if (textResult.clean) {
912
+ mergedFiles.set(path, textResult.merged);
913
+ continue;
914
+ }
915
+ conflicts.push({
916
+ path,
917
+ kind: "add-add",
918
+ ours: oursContent,
919
+ theirs: theirsContent,
920
+ mergedWithMarkers: textResult.merged
921
+ });
922
+ } else {
923
+ conflicts.push({ path, kind: "add-add", ours: oursContent, theirs: theirsContent });
924
+ }
925
+ continue;
926
+ }
927
+ if (oursExists && !theirsExists) {
928
+ conflicts.push({
929
+ path,
930
+ kind: "modify-delete",
931
+ ours: oursHash && blobStore ? blobStore.get(oursHash)?.toString("utf-8") : undefined
932
+ });
933
+ continue;
934
+ }
935
+ if (!oursExists && theirsExists) {
936
+ conflicts.push({
937
+ path,
938
+ kind: "modify-delete",
939
+ theirs: theirsHash && blobStore ? blobStore.get(theirsHash)?.toString("utf-8") : undefined
940
+ });
941
+ continue;
942
+ }
943
+ if (oursExists && theirsExists && oursHash !== theirsHash) {
944
+ const baseContent = baseHash && blobStore ? blobStore.get(baseHash)?.toString("utf-8") : undefined;
945
+ const oursContent = oursHash && blobStore ? blobStore.get(oursHash)?.toString("utf-8") : undefined;
946
+ const theirsContent = theirsHash && blobStore ? blobStore.get(theirsHash)?.toString("utf-8") : undefined;
947
+ if (baseContent !== undefined && oursContent !== undefined && theirsContent !== undefined) {
948
+ const textResult = threeWayTextMerge(baseContent, oursContent, theirsContent);
949
+ if (textResult.clean) {
950
+ mergedFiles.set(path, textResult.merged);
951
+ } else {
952
+ conflicts.push({
953
+ path,
954
+ kind: "modify-modify",
955
+ base: baseContent,
956
+ ours: oursContent,
957
+ theirs: theirsContent,
958
+ mergedWithMarkers: textResult.merged
959
+ });
960
+ }
961
+ } else {
962
+ conflicts.push({
963
+ path,
964
+ kind: "modify-modify",
965
+ base: baseContent,
966
+ ours: oursContent,
967
+ theirs: theirsContent
968
+ });
969
+ }
970
+ continue;
971
+ }
972
+ }
973
+ const added = [...mergedFiles.values()].filter((v) => v !== null).length;
974
+ const deleted = [...mergedFiles.values()].filter((v) => v === null).length;
975
+ return {
976
+ clean: conflicts.length === 0,
977
+ mergedFiles,
978
+ conflicts,
979
+ stats: {
980
+ added,
981
+ modified: added,
982
+ deleted,
983
+ conflicted: conflicts.length
984
+ }
985
+ };
986
+ }
987
+ function threeWayTextMerge(baseText, oursText, theirsText) {
988
+ const baseLines = baseText.split(`
989
+ `);
990
+ const oursLines = oursText.split(`
991
+ `);
992
+ const theirsLines = theirsText.split(`
993
+ `);
994
+ const oursChanges = computeLineChanges(baseLines, oursLines);
995
+ const theirsChanges = computeLineChanges(baseLines, theirsLines);
996
+ const result = [];
997
+ let clean = true;
998
+ let baseIdx = 0;
999
+ let oursIdx = 0;
1000
+ let theirsIdx = 0;
1001
+ while (baseIdx < baseLines.length || oursIdx < oursLines.length || theirsIdx < theirsLines.length) {
1002
+ const oursChange = oursChanges.get(baseIdx);
1003
+ const theirsChange = theirsChanges.get(baseIdx);
1004
+ if (baseIdx >= baseLines.length) {
1005
+ while (oursIdx < oursLines.length) {
1006
+ result.push(oursLines[oursIdx++]);
1007
+ }
1008
+ while (theirsIdx < theirsLines.length) {
1009
+ result.push(theirsLines[theirsIdx++]);
1010
+ }
1011
+ break;
1012
+ }
1013
+ if (!oursChange && !theirsChange) {
1014
+ result.push(baseLines[baseIdx]);
1015
+ baseIdx++;
1016
+ oursIdx++;
1017
+ theirsIdx++;
1018
+ } else if (oursChange && !theirsChange) {
1019
+ applyChange(oursChange, result);
1020
+ baseIdx += oursChange.baseCount;
1021
+ oursIdx += oursChange.newCount;
1022
+ theirsIdx += oursChange.baseCount;
1023
+ } else if (!oursChange && theirsChange) {
1024
+ applyChange(theirsChange, result);
1025
+ baseIdx += theirsChange.baseCount;
1026
+ oursIdx += theirsChange.baseCount;
1027
+ theirsIdx += theirsChange.newCount;
1028
+ } else if (oursChange && theirsChange) {
1029
+ if (oursChange.baseCount === theirsChange.baseCount && oursChange.newLines.join(`
1030
+ `) === theirsChange.newLines.join(`
1031
+ `)) {
1032
+ applyChange(oursChange, result);
1033
+ baseIdx += oursChange.baseCount;
1034
+ oursIdx += oursChange.newCount;
1035
+ theirsIdx += theirsChange.newCount;
1036
+ } else {
1037
+ clean = false;
1038
+ result.push("<<<<<<< ours");
1039
+ for (const line of oursChange.newLines)
1040
+ result.push(line);
1041
+ result.push("=======");
1042
+ for (const line of theirsChange.newLines)
1043
+ result.push(line);
1044
+ result.push(">>>>>>> theirs");
1045
+ baseIdx += Math.max(oursChange.baseCount, theirsChange.baseCount);
1046
+ oursIdx += oursChange.newCount;
1047
+ theirsIdx += theirsChange.newCount;
1048
+ }
1049
+ }
1050
+ }
1051
+ return { clean, merged: result.join(`
1052
+ `) };
1053
+ }
1054
+ function applyChange(change, result) {
1055
+ for (const line of change.newLines) {
1056
+ result.push(line);
1057
+ }
1058
+ }
1059
+ function computeLineChanges(baseLines, newLines) {
1060
+ const changes = new Map;
1061
+ const matches = lcsMatch(baseLines, newLines);
1062
+ let baseIdx = 0;
1063
+ let newIdx = 0;
1064
+ for (const match of matches) {
1065
+ if (baseIdx < match.baseIdx || newIdx < match.newIdx) {
1066
+ const baseCount = match.baseIdx - baseIdx;
1067
+ const newCount = match.newIdx - newIdx;
1068
+ if (baseCount > 0 || newCount > 0) {
1069
+ changes.set(baseIdx, {
1070
+ baseStart: baseIdx,
1071
+ baseCount,
1072
+ newCount,
1073
+ newLines: newLines.slice(newIdx, newIdx + newCount)
1074
+ });
1075
+ }
1076
+ }
1077
+ baseIdx = match.baseIdx + 1;
1078
+ newIdx = match.newIdx + 1;
1079
+ }
1080
+ if (baseIdx < baseLines.length || newIdx < newLines.length) {
1081
+ const baseCount = baseLines.length - baseIdx;
1082
+ const newCount = newLines.length - newIdx;
1083
+ if (baseCount > 0 || newCount > 0) {
1084
+ changes.set(baseIdx, {
1085
+ baseStart: baseIdx,
1086
+ baseCount,
1087
+ newCount,
1088
+ newLines: newLines.slice(newIdx)
1089
+ });
1090
+ }
1091
+ }
1092
+ return changes;
1093
+ }
1094
+ function lcsMatch(a, b) {
1095
+ const n = a.length;
1096
+ const m = b.length;
1097
+ if (n === 0 || m === 0)
1098
+ return [];
1099
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
1100
+ for (let i2 = 1;i2 <= n; i2++) {
1101
+ for (let j2 = 1;j2 <= m; j2++) {
1102
+ if (a[i2 - 1] === b[j2 - 1]) {
1103
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
1104
+ } else {
1105
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
1106
+ }
1107
+ }
1108
+ }
1109
+ const matches = [];
1110
+ let i = n;
1111
+ let j = m;
1112
+ while (i > 0 && j > 0) {
1113
+ if (a[i - 1] === b[j - 1]) {
1114
+ matches.unshift({ baseIdx: i - 1, newIdx: j - 1 });
1115
+ i--;
1116
+ j--;
1117
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
1118
+ i--;
1119
+ } else {
1120
+ j--;
1121
+ }
1122
+ }
1123
+ return matches;
1124
+ }
1125
+
1126
+ // src/vcs/issue.ts
1127
+ import { exec } from "child_process";
1128
+ import { promisify } from "util";
1129
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
1130
+ import { join as join3, dirname as dirname2 } from "path";
1131
+ var execAsync = promisify(exec);
1132
+ function getIssueCounterPath(rootPath) {
1133
+ return join3(rootPath, ".trellis", "issue-counter.json");
1134
+ }
1135
+ function nextIssueId(rootPath) {
1136
+ const counterPath = getIssueCounterPath(rootPath);
1137
+ let counter = 0;
1138
+ if (existsSync3(counterPath)) {
1139
+ try {
1140
+ counter = JSON.parse(readFileSync3(counterPath, "utf-8")).counter ?? 0;
1141
+ } catch {}
1142
+ }
1143
+ counter++;
1144
+ const dir = dirname2(counterPath);
1145
+ if (!existsSync3(dir))
1146
+ mkdirSync2(dir, { recursive: true });
1147
+ writeFileSync3(counterPath, JSON.stringify({ counter }, null, 2));
1148
+ return `TRL-${counter}`;
1149
+ }
1150
+ function getIssueFact(ctx, entityId, attr) {
1151
+ const facts = ctx.store.getFactsByEntity(entityId);
1152
+ const matches = facts.filter((f) => f.a === attr);
1153
+ return matches.length > 0 ? matches[matches.length - 1].v : undefined;
1154
+ }
1155
+ function getIssueLinks(ctx, entityId, attr) {
1156
+ const links = ctx.store.getLinksByEntity(entityId);
1157
+ return links.filter((l) => l.a === attr && l.e1 === entityId).map((l) => l.e2);
1158
+ }
1159
+ function getCriteriaForIssue(ctx, issueId) {
1160
+ const eid = issueEntityId(issueId);
1161
+ const criterionLinks = ctx.store.getLinksByAttribute("criterionOf").filter((l) => l.e2 === eid);
1162
+ return criterionLinks.map((link) => {
1163
+ const ceid = link.e1;
1164
+ const facts = ctx.store.getFactsByEntity(ceid);
1165
+ const getLast = (a) => {
1166
+ const matches = facts.filter((f) => f.a === a);
1167
+ return matches.length > 0 ? matches[matches.length - 1].v : undefined;
1168
+ };
1169
+ return {
1170
+ id: ceid,
1171
+ description: getLast("description"),
1172
+ command: getLast("command"),
1173
+ status: getLast("status"),
1174
+ lastRunAt: getLast("lastRunAt"),
1175
+ lastOutput: getLast("lastOutput")
1176
+ };
1177
+ });
1178
+ }
1179
+ function buildIssueInfo(ctx, entityId) {
1180
+ const facts = ctx.store.getFactsByEntity(entityId);
1181
+ const get = (a) => {
1182
+ const matches = facts.filter((f) => f.a === a);
1183
+ return matches.length > 0 ? matches[matches.length - 1].v : undefined;
1184
+ };
1185
+ const labelsStr = get("labels");
1186
+ const labels = labelsStr ? labelsStr.split(",").filter(Boolean) : [];
1187
+ const trackedOnLinks = getIssueLinks(ctx, entityId, "trackedOn");
1188
+ const branchName = trackedOnLinks.length > 0 ? trackedOnLinks[0].replace(/^branch:/, "") : undefined;
1189
+ const childOfLinks = getIssueLinks(ctx, entityId, "childOf");
1190
+ const parentId = childOfLinks.length > 0 ? childOfLinks[0].replace(/^issue:/, "") : undefined;
1191
+ const bareId = entityId.replace(/^issue:/, "");
1192
+ const blockedByLinks = getIssueLinks(ctx, entityId, "blockedBy");
1193
+ const blockedBy = blockedByLinks.map((e) => e.replace(/^issue:/, ""));
1194
+ const allBlockedByLinks = ctx.store.getLinksByAttribute("blockedBy");
1195
+ const blocking = allBlockedByLinks.filter((l) => l.e2 === entityId).map((l) => l.e1.replace(/^issue:/, ""));
1196
+ const isBlocked = blockedByLinks.some((blockerEid) => {
1197
+ const blockerStatus = getIssueFact(ctx, blockerEid, "status");
1198
+ return blockerStatus !== "closed";
1199
+ });
1200
+ return {
1201
+ id: bareId,
1202
+ title: get("title"),
1203
+ description: get("description"),
1204
+ status: get("status"),
1205
+ priority: get("priority"),
1206
+ labels,
1207
+ assignee: get("assignee"),
1208
+ createdAt: get("createdAt"),
1209
+ createdBy: get("createdBy"),
1210
+ startedAt: get("startedAt"),
1211
+ pausedAt: get("pausedAt"),
1212
+ pauseNote: get("pauseNote") || undefined,
1213
+ closedAt: get("closedAt"),
1214
+ parentId,
1215
+ branchName,
1216
+ blockedBy,
1217
+ blocking,
1218
+ isBlocked,
1219
+ criteria: getCriteriaForIssue(ctx, bareId)
1220
+ };
1221
+ }
1222
+ async function createIssue(ctx, rootPath, title, opts) {
1223
+ const id = nextIssueId(rootPath);
1224
+ const op = await createVcsOp("vcs:issueCreate", {
1225
+ agentId: ctx.agentId,
1226
+ previousHash: ctx.getLastOp()?.hash,
1227
+ vcs: {
1228
+ issueId: id,
1229
+ issueTitle: title,
1230
+ issueDescription: opts?.description,
1231
+ issueStatus: opts?.status ?? "backlog",
1232
+ issuePriority: opts?.priority ?? "medium",
1233
+ issueLabels: opts?.labels,
1234
+ issueAssignee: opts?.assignee,
1235
+ parentIssueId: opts?.parentId
1236
+ }
1237
+ });
1238
+ ctx.applyOp(op);
1239
+ if (opts?.criteria) {
1240
+ for (let i = 0;i < opts.criteria.length; i++) {
1241
+ await addCriterion(ctx, id, opts.criteria[i].description, opts.criteria[i].command);
1242
+ }
1243
+ }
1244
+ return op;
1245
+ }
1246
+ async function updateIssue(ctx, id, updates) {
1247
+ const op = await createVcsOp("vcs:issueUpdate", {
1248
+ agentId: ctx.agentId,
1249
+ previousHash: ctx.getLastOp()?.hash,
1250
+ vcs: {
1251
+ issueId: id,
1252
+ issueTitle: updates.title,
1253
+ issueDescription: updates.description,
1254
+ issueStatus: updates.status,
1255
+ issuePriority: updates.priority,
1256
+ issueLabels: updates.labels,
1257
+ issueAssignee: updates.assignee
1258
+ }
1259
+ });
1260
+ ctx.applyOp(op);
1261
+ return op;
1262
+ }
1263
+ async function startIssue(ctx, id, branchName) {
1264
+ const eid = issueEntityId(id);
1265
+ const status = getIssueFact(ctx, eid, "status");
1266
+ if (status === "closed") {
1267
+ throw new Error(`Cannot start closed issue ${id}. Reopen it first.`);
1268
+ }
1269
+ if (status === "in_progress") {
1270
+ throw new Error(`Issue ${id} is already in progress.`);
1271
+ }
1272
+ const op = await createVcsOp("vcs:issueStart", {
1273
+ agentId: ctx.agentId,
1274
+ previousHash: ctx.getLastOp()?.hash,
1275
+ vcs: {
1276
+ issueId: id,
1277
+ issueAssignee: ctx.agentId,
1278
+ branchName
1279
+ }
1280
+ });
1281
+ ctx.applyOp(op);
1282
+ return op;
1283
+ }
1284
+ async function pauseIssue(ctx, id, note) {
1285
+ if (!note || !note.trim()) {
1286
+ throw new Error(`A pause note is required. Explain why the issue is paused and what must happen before resuming.`);
1287
+ }
1288
+ const eid = issueEntityId(id);
1289
+ const status = getIssueFact(ctx, eid, "status");
1290
+ if (status !== "in_progress") {
1291
+ throw new Error(`Cannot pause issue ${id} \u2014 status is '${status}', expected 'in_progress'.`);
1292
+ }
1293
+ const op = await createVcsOp("vcs:issuePause", {
1294
+ agentId: ctx.agentId,
1295
+ previousHash: ctx.getLastOp()?.hash,
1296
+ vcs: { issueId: id, pauseNote: note.trim() }
1297
+ });
1298
+ ctx.applyOp(op);
1299
+ return op;
1300
+ }
1301
+ async function resumeIssue(ctx, id) {
1302
+ const eid = issueEntityId(id);
1303
+ const status = getIssueFact(ctx, eid, "status");
1304
+ if (status !== "paused") {
1305
+ throw new Error(`Cannot resume issue ${id} \u2014 status is '${status}', expected 'paused'.`);
1306
+ }
1307
+ const op = await createVcsOp("vcs:issueResume", {
1308
+ agentId: ctx.agentId,
1309
+ previousHash: ctx.getLastOp()?.hash,
1310
+ vcs: { issueId: id }
1311
+ });
1312
+ ctx.applyOp(op);
1313
+ return op;
1314
+ }
1315
+ async function closeIssue(ctx, id, opts) {
1316
+ const eid = issueEntityId(id);
1317
+ const status = getIssueFact(ctx, eid, "status");
1318
+ if (status === "closed") {
1319
+ throw new Error(`Issue ${id} is already closed.`);
1320
+ }
1321
+ const criteria = getCriteriaForIssue(ctx, id);
1322
+ const results = criteria.map((c) => ({
1323
+ id: c.id,
1324
+ description: c.description,
1325
+ command: c.command,
1326
+ status: c.status ?? "pending"
1327
+ }));
1328
+ const allPassed = results.length === 0 || results.every((r) => r.status === "passed");
1329
+ if (!allPassed) {
1330
+ const failing = results.filter((r) => r.status !== "passed");
1331
+ throw new Error(`Cannot close issue ${id}: ${failing.length} criteria not passing:
1332
+ ` + failing.map((f) => ` - ${f.description ?? f.id} (${f.status})`).join(`
1333
+ `));
1334
+ }
1335
+ if (!opts?.confirm) {
1336
+ return { criteriaResults: results };
1337
+ }
1338
+ const startedAt = getIssueFact(ctx, eid, "startedAt");
1339
+ const op = await createVcsOp("vcs:issueClose", {
1340
+ agentId: ctx.agentId,
1341
+ previousHash: ctx.getLastOp()?.hash,
1342
+ vcs: { issueId: id }
1343
+ });
1344
+ ctx.applyOp(op);
1345
+ if (startedAt) {
1346
+ const durationMs = Date.now() - new Date(startedAt).getTime();
1347
+ ctx.store.addFacts([{ e: eid, a: "durationMs", v: durationMs }]);
1348
+ }
1349
+ return { op, criteriaResults: results };
1350
+ }
1351
+ async function triageIssue(ctx, id) {
1352
+ const eid = issueEntityId(id);
1353
+ const status = getIssueFact(ctx, eid, "status");
1354
+ if (status !== "backlog") {
1355
+ throw new Error(`Cannot triage issue ${id} \u2014 status is '${status}', expected 'backlog'.`);
1356
+ }
1357
+ const op = await createVcsOp("vcs:issueUpdate", {
1358
+ agentId: ctx.agentId,
1359
+ previousHash: ctx.getLastOp()?.hash,
1360
+ vcs: {
1361
+ issueId: id,
1362
+ issueStatus: "queue"
1363
+ }
1364
+ });
1365
+ ctx.applyOp(op);
1366
+ return op;
1367
+ }
1368
+ async function reopenIssue(ctx, id) {
1369
+ const eid = issueEntityId(id);
1370
+ const status = getIssueFact(ctx, eid, "status");
1371
+ if (status !== "closed") {
1372
+ throw new Error(`Cannot reopen issue ${id} \u2014 status is '${status}', expected 'closed'.`);
1373
+ }
1374
+ const op = await createVcsOp("vcs:issueReopen", {
1375
+ agentId: ctx.agentId,
1376
+ previousHash: ctx.getLastOp()?.hash,
1377
+ vcs: { issueId: id }
1378
+ });
1379
+ ctx.applyOp(op);
1380
+ return op;
1381
+ }
1382
+ async function assignIssue(ctx, id, agentId) {
1383
+ return updateIssue(ctx, id, { assignee: agentId });
1384
+ }
1385
+ async function blockIssue(ctx, id, blockedById) {
1386
+ const eid = issueEntityId(id);
1387
+ const blockerEid = issueEntityId(blockedById);
1388
+ if (!getIssueFact(ctx, eid, "type")) {
1389
+ throw new Error(`Issue ${id} not found.`);
1390
+ }
1391
+ if (!getIssueFact(ctx, blockerEid, "type")) {
1392
+ throw new Error(`Blocking issue ${blockedById} not found.`);
1393
+ }
1394
+ if (id === blockedById) {
1395
+ throw new Error(`Issue cannot block itself.`);
1396
+ }
1397
+ const op = await createVcsOp("vcs:issueBlock", {
1398
+ agentId: ctx.agentId,
1399
+ previousHash: ctx.getLastOp()?.hash,
1400
+ vcs: { issueId: id, blockedByIssueId: blockedById }
1401
+ });
1402
+ ctx.applyOp(op);
1403
+ return op;
1404
+ }
1405
+ async function unblockIssue(ctx, id, blockedById) {
1406
+ const op = await createVcsOp("vcs:issueUnblock", {
1407
+ agentId: ctx.agentId,
1408
+ previousHash: ctx.getLastOp()?.hash,
1409
+ vcs: { issueId: id, blockedByIssueId: blockedById }
1410
+ });
1411
+ ctx.applyOp(op);
1412
+ return op;
1413
+ }
1414
+ async function addCriterion(ctx, issueId, description, command) {
1415
+ const existing = getCriteriaForIssue(ctx, issueId);
1416
+ const index = existing.length + 1;
1417
+ const cid = criterionEntityId(issueId, index);
1418
+ const op = await createVcsOp("vcs:criterionAdd", {
1419
+ agentId: ctx.agentId,
1420
+ previousHash: ctx.getLastOp()?.hash,
1421
+ vcs: {
1422
+ issueId,
1423
+ criterionId: cid,
1424
+ criterionDescription: description,
1425
+ criterionCommand: command
1426
+ }
1427
+ });
1428
+ ctx.applyOp(op);
1429
+ return op;
1430
+ }
1431
+ async function setCriterionStatus(ctx, issueId, criterionIndex, status) {
1432
+ const criteria = getCriteriaForIssue(ctx, issueId);
1433
+ if (criterionIndex < 1 || criterionIndex > criteria.length) {
1434
+ throw new Error(`Criterion index ${criterionIndex} out of range (1\u2013${criteria.length})`);
1435
+ }
1436
+ const c = criteria[criterionIndex - 1];
1437
+ const op = await createVcsOp("vcs:criterionUpdate", {
1438
+ agentId: ctx.agentId,
1439
+ previousHash: ctx.getLastOp()?.hash,
1440
+ vcs: {
1441
+ issueId,
1442
+ criterionId: c.id,
1443
+ criterionStatus: status
1444
+ }
1445
+ });
1446
+ ctx.applyOp(op);
1447
+ return op;
1448
+ }
1449
+ async function runCriteria(ctx, issueId, rootPath) {
1450
+ const criteria = getCriteriaForIssue(ctx, issueId);
1451
+ const results = [];
1452
+ for (const c of criteria) {
1453
+ if (!c.command) {
1454
+ results.push({
1455
+ id: c.id,
1456
+ description: c.description,
1457
+ status: c.status ?? "skipped"
1458
+ });
1459
+ continue;
1460
+ }
1461
+ let status = "failed";
1462
+ let output = "";
1463
+ let exitCode = 1;
1464
+ try {
1465
+ const result = await execAsync(c.command, {
1466
+ cwd: rootPath,
1467
+ timeout: 120000
1468
+ });
1469
+ output = (result.stdout + `
1470
+ ` + result.stderr).trim();
1471
+ exitCode = 0;
1472
+ status = "passed";
1473
+ } catch (err) {
1474
+ output = (err.stdout ?? "") + `
1475
+ ` + (err.stderr ?? err.message ?? "");
1476
+ output = output.trim();
1477
+ exitCode = err.code ?? 1;
1478
+ status = "failed";
1479
+ }
1480
+ const updateOp = await createVcsOp("vcs:criterionUpdate", {
1481
+ agentId: ctx.agentId,
1482
+ previousHash: ctx.getLastOp()?.hash,
1483
+ vcs: {
1484
+ criterionId: c.id,
1485
+ criterionStatus: status,
1486
+ criterionOutput: output.slice(0, 4096)
1487
+ }
1488
+ });
1489
+ ctx.applyOp(updateOp);
1490
+ results.push({
1491
+ id: c.id,
1492
+ description: c.description,
1493
+ command: c.command,
1494
+ status,
1495
+ output,
1496
+ exitCode
1497
+ });
1498
+ }
1499
+ return results;
1500
+ }
1501
+ function listIssues(ctx, filters) {
1502
+ const issueFacts = ctx.store.getFactsByAttribute("type").filter((f) => f.v === "Issue");
1503
+ let issues = issueFacts.map((f) => buildIssueInfo(ctx, f.e));
1504
+ if (filters?.status) {
1505
+ issues = issues.filter((i) => i.status === filters.status);
1506
+ }
1507
+ if (filters?.assignee) {
1508
+ issues = issues.filter((i) => i.assignee === filters.assignee);
1509
+ }
1510
+ if (filters?.label) {
1511
+ issues = issues.filter((i) => i.labels.includes(filters.label));
1512
+ }
1513
+ if (filters?.parentId) {
1514
+ issues = issues.filter((i) => i.parentId === filters.parentId);
1515
+ }
1516
+ if (filters?.blocked !== undefined) {
1517
+ issues = issues.filter((i) => i.isBlocked === filters.blocked);
1518
+ }
1519
+ return issues;
1520
+ }
1521
+ function getIssue(ctx, id) {
1522
+ const eid = issueEntityId(id);
1523
+ const typeFact = ctx.store.getFactsByEntity(eid).find((f) => f.a === "type" && f.v === "Issue");
1524
+ if (!typeFact)
1525
+ return null;
1526
+ return buildIssueInfo(ctx, eid);
1527
+ }
1528
+ function getActiveIssues(ctx) {
1529
+ return listIssues(ctx, { status: "in_progress" });
1530
+ }
1531
+ function checkCompletionReadiness(ctx) {
1532
+ const all = listIssues(ctx);
1533
+ const queue = all.filter((i) => i.status === "queue");
1534
+ const paused = all.filter((i) => i.status === "paused");
1535
+ const inProgress = all.filter((i) => i.status === "in_progress");
1536
+ const ready = queue.length === 0 && paused.length === 0 && inProgress.length === 0;
1537
+ const parts = [];
1538
+ if (ready) {
1539
+ parts.push("\u2713 All clear \u2014 no queue, paused, or in-progress issues.");
1540
+ } else {
1541
+ parts.push("\u2717 Not ready for completion:");
1542
+ if (queue.length > 0) {
1543
+ parts.push(` Queue (${queue.length}): ${queue.map((i) => i.id).join(", ")}`);
1544
+ }
1545
+ if (inProgress.length > 0) {
1546
+ parts.push(` In progress (${inProgress.length}): ${inProgress.map((i) => i.id).join(", ")}`);
1547
+ }
1548
+ if (paused.length > 0) {
1549
+ parts.push(` Paused (${paused.length}): ${paused.map((i) => `${i.id}${i.pauseNote ? ` \u2014 ${i.pauseNote}` : ""}`).join(", ")}`);
1550
+ }
1551
+ }
1552
+ return { ready, queue, paused, inProgress, summary: parts.join(`
1553
+ `) };
1554
+ }
1555
+
1556
+ export { decompose, BlobStore, createBranch, switchBranch, listBranches, deleteBranch, saveBranchState, loadBranchState, createMilestone, listMilestones, createCheckpoint, listCheckpoints, diffFileStates, buildFileStateAtOp, diffOpRange, generateUnifiedDiff, myersDiff, threeWayMerge, threeWayTextMerge, createIssue, updateIssue, startIssue, pauseIssue, resumeIssue, closeIssue, triageIssue, reopenIssue, assignIssue, blockIssue, unblockIssue, addCriterion, setCriterionStatus, runCriteria, listIssues, getIssue, getActiveIssues, checkCompletionReadiness };