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.
- package/LICENSE +21 -0
- package/README.md +564 -83
- package/bin/trellis.mjs +2 -0
- package/dist/cli/index.js +4718 -0
- package/dist/core/index.js +12 -0
- package/dist/decisions/index.js +19 -0
- package/dist/embeddings/index.js +43 -0
- package/dist/index-1j1anhmr.js +4038 -0
- package/dist/index-3s0eak0p.js +1556 -0
- package/dist/index-8pce39mh.js +272 -0
- package/dist/index-a76rekgs.js +67 -0
- package/dist/index-cy9k1g6v.js +684 -0
- package/dist/index-fd4e26s4.js +69 -0
- package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
- package/dist/index-gnw8d7d6.js +51 -0
- package/dist/index-vkpkfwhq.js +817 -0
- package/dist/index.js +118 -2876
- package/dist/links/index.js +55 -0
- package/dist/transformers-m9je15kg.js +32491 -0
- package/dist/vcs/index.js +110 -0
- package/logo.png +0 -0
- package/logo.svg +9 -0
- package/package.json +79 -76
- package/src/cli/index.ts +2340 -0
- package/src/core/index.ts +35 -0
- package/src/core/kernel/middleware.ts +44 -0
- package/src/core/persist/backend.ts +64 -0
- package/src/core/store/eav-store.ts +467 -0
- package/src/decisions/auto-capture.ts +136 -0
- package/src/decisions/hooks.ts +163 -0
- package/src/decisions/index.ts +261 -0
- package/src/decisions/types.ts +103 -0
- package/src/embeddings/chunker.ts +327 -0
- package/src/embeddings/index.ts +41 -0
- package/src/embeddings/model.ts +95 -0
- package/src/embeddings/search.ts +305 -0
- package/src/embeddings/store.ts +313 -0
- package/src/embeddings/types.ts +85 -0
- package/src/engine.ts +1083 -0
- package/src/garden/cluster.ts +330 -0
- package/src/garden/garden.ts +306 -0
- package/src/garden/index.ts +29 -0
- package/src/git/git-exporter.ts +286 -0
- package/src/git/git-importer.ts +329 -0
- package/src/git/git-reader.ts +189 -0
- package/src/git/index.ts +22 -0
- package/src/identity/governance.ts +211 -0
- package/src/identity/identity.ts +224 -0
- package/src/identity/index.ts +30 -0
- package/src/identity/signing-middleware.ts +97 -0
- package/src/index.ts +20 -0
- package/src/links/index.ts +49 -0
- package/src/links/lifecycle.ts +400 -0
- package/src/links/parser.ts +484 -0
- package/src/links/ref-index.ts +186 -0
- package/src/links/resolver.ts +314 -0
- package/src/links/types.ts +108 -0
- package/src/mcp/index.ts +22 -0
- package/src/mcp/server.ts +1278 -0
- package/src/semantic/csharp-parser.ts +493 -0
- package/src/semantic/go-parser.ts +585 -0
- package/src/semantic/index.ts +34 -0
- package/src/semantic/java-parser.ts +456 -0
- package/src/semantic/python-parser.ts +659 -0
- package/src/semantic/ruby-parser.ts +446 -0
- package/src/semantic/rust-parser.ts +784 -0
- package/src/semantic/semantic-merge.ts +210 -0
- package/src/semantic/ts-parser.ts +681 -0
- package/src/semantic/types.ts +175 -0
- package/src/sync/index.ts +32 -0
- package/src/sync/memory-transport.ts +66 -0
- package/src/sync/reconciler.ts +237 -0
- package/src/sync/sync-engine.ts +258 -0
- package/src/sync/types.ts +104 -0
- package/src/vcs/blob-store.ts +124 -0
- package/src/vcs/branch.ts +150 -0
- package/src/vcs/checkpoint.ts +64 -0
- package/src/vcs/decompose.ts +469 -0
- package/src/vcs/diff.ts +409 -0
- package/src/vcs/engine-context.ts +26 -0
- package/src/vcs/index.ts +23 -0
- package/src/vcs/issue.ts +800 -0
- package/src/vcs/merge.ts +425 -0
- package/src/vcs/milestone.ts +124 -0
- package/src/vcs/ops.ts +59 -0
- package/src/vcs/types.ts +213 -0
- package/src/vcs/vcs-middleware.ts +81 -0
- package/src/watcher/fs-watcher.ts +217 -0
- package/src/watcher/index.ts +9 -0
- package/src/watcher/ingestion.ts +116 -0
- package/dist/ai/index.js +0 -688
- package/dist/cli/server.js +0 -3321
- package/dist/cli/tql.js +0 -5282
- package/dist/client/tql-client.js +0 -108
- package/dist/graph/index.js +0 -2248
- package/dist/kernel/logic-middleware.js +0 -179
- package/dist/kernel/middleware.js +0 -0
- package/dist/kernel/operations.js +0 -32
- package/dist/kernel/schema-middleware.js +0 -34
- package/dist/kernel/security-middleware.js +0 -53
- package/dist/kernel/trellis-kernel.js +0 -2239
- package/dist/kernel/workspace.js +0 -91
- package/dist/persist/backend.js +0 -0
- package/dist/persist/sqlite-backend.js +0 -123
- package/dist/query/index.js +0 -1643
- package/dist/server/index.js +0 -3309
- 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 };
|