trellis 2.0.13 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- package/src/watcher/ingestion.ts +0 -116
package/src/vcs/issue.ts
DELETED
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue Module
|
|
3
|
-
*
|
|
4
|
-
* Extracted per DESIGN.md pattern (like milestone.ts, branch.ts).
|
|
5
|
-
* Handles issue creation, lifecycle (start/pause/resume/close/reopen),
|
|
6
|
-
* acceptance criteria, and queries.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { exec } from 'child_process';
|
|
10
|
-
import { promisify } from 'util';
|
|
11
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
-
import { join, dirname } from 'path';
|
|
13
|
-
import { createVcsOp } from './ops.js';
|
|
14
|
-
import type { VcsOp } from './types.js';
|
|
15
|
-
import { issueEntityId, criterionEntityId } from './types.js';
|
|
16
|
-
import type { EngineContext } from './engine-context.js';
|
|
17
|
-
|
|
18
|
-
const execAsync = promisify(exec);
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Types
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export interface IssueInfo {
|
|
25
|
-
id: string;
|
|
26
|
-
title?: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
status?: string;
|
|
29
|
-
priority?: string;
|
|
30
|
-
labels: string[];
|
|
31
|
-
assignee?: string;
|
|
32
|
-
createdAt?: string;
|
|
33
|
-
createdBy?: string;
|
|
34
|
-
startedAt?: string;
|
|
35
|
-
pausedAt?: string;
|
|
36
|
-
pauseNote?: string;
|
|
37
|
-
closedAt?: string;
|
|
38
|
-
parentId?: string;
|
|
39
|
-
branchName?: string;
|
|
40
|
-
blockedBy: string[];
|
|
41
|
-
blocking: string[];
|
|
42
|
-
isBlocked: boolean;
|
|
43
|
-
criteria: CriterionInfo[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface CriterionInfo {
|
|
47
|
-
id: string;
|
|
48
|
-
description?: string;
|
|
49
|
-
command?: string;
|
|
50
|
-
status?: string;
|
|
51
|
-
lastRunAt?: string;
|
|
52
|
-
lastOutput?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface CriterionResult {
|
|
56
|
-
id: string;
|
|
57
|
-
description?: string;
|
|
58
|
-
command?: string;
|
|
59
|
-
status: 'passed' | 'failed' | 'skipped';
|
|
60
|
-
output?: string;
|
|
61
|
-
exitCode?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface IssueFilters {
|
|
65
|
-
status?: string;
|
|
66
|
-
assignee?: string;
|
|
67
|
-
label?: string;
|
|
68
|
-
parentId?: string;
|
|
69
|
-
blocked?: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// ID generation
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
function getIssueCounterPath(rootPath: string): string {
|
|
77
|
-
return join(rootPath, '.trellis', 'issue-counter.json');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function nextIssueId(rootPath: string): string {
|
|
81
|
-
const counterPath = getIssueCounterPath(rootPath);
|
|
82
|
-
let counter = 0;
|
|
83
|
-
if (existsSync(counterPath)) {
|
|
84
|
-
try {
|
|
85
|
-
counter = JSON.parse(readFileSync(counterPath, 'utf-8')).counter ?? 0;
|
|
86
|
-
} catch {}
|
|
87
|
-
}
|
|
88
|
-
counter++;
|
|
89
|
-
const dir = dirname(counterPath);
|
|
90
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
91
|
-
writeFileSync(counterPath, JSON.stringify({ counter }, null, 2));
|
|
92
|
-
return `TRL-${counter}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function slugify(text: string): string {
|
|
96
|
-
return text
|
|
97
|
-
.toLowerCase()
|
|
98
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
99
|
-
.replace(/^-|-$/g, '')
|
|
100
|
-
.slice(0, 40);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Helpers
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
function getIssueFact(
|
|
108
|
-
ctx: EngineContext,
|
|
109
|
-
entityId: string,
|
|
110
|
-
attr: string,
|
|
111
|
-
): string | undefined {
|
|
112
|
-
const facts = ctx.store.getFactsByEntity(entityId);
|
|
113
|
-
// Return the LAST matching fact — EAV store appends, so latest is authoritative
|
|
114
|
-
const matches = facts.filter((f) => f.a === attr);
|
|
115
|
-
return matches.length > 0
|
|
116
|
-
? (matches[matches.length - 1].v as string)
|
|
117
|
-
: undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getIssueLinks(
|
|
121
|
-
ctx: EngineContext,
|
|
122
|
-
entityId: string,
|
|
123
|
-
attr: string,
|
|
124
|
-
): string[] {
|
|
125
|
-
const links = ctx.store.getLinksByEntity(entityId);
|
|
126
|
-
// Only forward links (e1 === entityId), not reverse
|
|
127
|
-
return links
|
|
128
|
-
.filter((l) => l.a === attr && l.e1 === entityId)
|
|
129
|
-
.map((l) => l.e2);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getCriteriaForIssue(
|
|
133
|
-
ctx: EngineContext,
|
|
134
|
-
issueId: string,
|
|
135
|
-
): CriterionInfo[] {
|
|
136
|
-
const eid = issueEntityId(issueId);
|
|
137
|
-
// Find all criterion entities linked to this issue
|
|
138
|
-
const criterionLinks = ctx.store
|
|
139
|
-
.getLinksByAttribute('criterionOf')
|
|
140
|
-
.filter((l) => l.e2 === eid);
|
|
141
|
-
|
|
142
|
-
return criterionLinks.map((link) => {
|
|
143
|
-
const ceid = link.e1;
|
|
144
|
-
const facts = ctx.store.getFactsByEntity(ceid);
|
|
145
|
-
const getLast = (a: string) => {
|
|
146
|
-
const matches = facts.filter((f) => f.a === a);
|
|
147
|
-
return matches.length > 0
|
|
148
|
-
? (matches[matches.length - 1].v as string)
|
|
149
|
-
: undefined;
|
|
150
|
-
};
|
|
151
|
-
return {
|
|
152
|
-
id: ceid,
|
|
153
|
-
description: getLast('description'),
|
|
154
|
-
command: getLast('command'),
|
|
155
|
-
status: getLast('status'),
|
|
156
|
-
lastRunAt: getLast('lastRunAt'),
|
|
157
|
-
lastOutput: getLast('lastOutput'),
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function buildIssueInfo(ctx: EngineContext, entityId: string): IssueInfo {
|
|
163
|
-
const facts = ctx.store.getFactsByEntity(entityId);
|
|
164
|
-
// Use last matching fact for each attribute (latest is authoritative)
|
|
165
|
-
const get = (a: string) => {
|
|
166
|
-
const matches = facts.filter((f) => f.a === a);
|
|
167
|
-
return matches.length > 0
|
|
168
|
-
? (matches[matches.length - 1].v as string)
|
|
169
|
-
: undefined;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const labelsStr = get('labels');
|
|
173
|
-
const labels = labelsStr ? labelsStr.split(',').filter(Boolean) : [];
|
|
174
|
-
|
|
175
|
-
const trackedOnLinks = getIssueLinks(ctx, entityId, 'trackedOn');
|
|
176
|
-
const branchName =
|
|
177
|
-
trackedOnLinks.length > 0
|
|
178
|
-
? trackedOnLinks[0].replace(/^branch:/, '')
|
|
179
|
-
: undefined;
|
|
180
|
-
|
|
181
|
-
const childOfLinks = getIssueLinks(ctx, entityId, 'childOf');
|
|
182
|
-
const parentId =
|
|
183
|
-
childOfLinks.length > 0
|
|
184
|
-
? childOfLinks[0].replace(/^issue:/, '')
|
|
185
|
-
: undefined;
|
|
186
|
-
|
|
187
|
-
const bareId = entityId.replace(/^issue:/, '');
|
|
188
|
-
|
|
189
|
-
// Blocking: forward blockedBy links (this issue is blocked by...)
|
|
190
|
-
const blockedByLinks = getIssueLinks(ctx, entityId, 'blockedBy');
|
|
191
|
-
const blockedBy = blockedByLinks.map((e) => e.replace(/^issue:/, ''));
|
|
192
|
-
|
|
193
|
-
// Blocking: reverse blockedBy links (this issue blocks...)
|
|
194
|
-
const allBlockedByLinks = ctx.store.getLinksByAttribute('blockedBy');
|
|
195
|
-
const blocking = allBlockedByLinks
|
|
196
|
-
.filter((l) => l.e2 === entityId)
|
|
197
|
-
.map((l) => l.e1.replace(/^issue:/, ''));
|
|
198
|
-
|
|
199
|
-
// Derived: isBlocked if any blocker is not closed
|
|
200
|
-
const isBlocked = blockedByLinks.some((blockerEid) => {
|
|
201
|
-
const blockerStatus = getIssueFact(ctx, blockerEid, 'status');
|
|
202
|
-
return blockerStatus !== 'closed';
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
id: bareId,
|
|
207
|
-
title: get('title'),
|
|
208
|
-
description: get('description'),
|
|
209
|
-
status: get('status'),
|
|
210
|
-
priority: get('priority'),
|
|
211
|
-
labels,
|
|
212
|
-
assignee: get('assignee'),
|
|
213
|
-
createdAt: get('createdAt'),
|
|
214
|
-
createdBy: get('createdBy'),
|
|
215
|
-
startedAt: get('startedAt'),
|
|
216
|
-
pausedAt: get('pausedAt'),
|
|
217
|
-
pauseNote: get('pauseNote') || undefined,
|
|
218
|
-
closedAt: get('closedAt'),
|
|
219
|
-
parentId,
|
|
220
|
-
branchName,
|
|
221
|
-
blockedBy,
|
|
222
|
-
blocking,
|
|
223
|
-
isBlocked,
|
|
224
|
-
criteria: getCriteriaForIssue(ctx, bareId),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
// Operations
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Create a new issue.
|
|
234
|
-
*/
|
|
235
|
-
export async function createIssue(
|
|
236
|
-
ctx: EngineContext,
|
|
237
|
-
rootPath: string,
|
|
238
|
-
title: string,
|
|
239
|
-
opts?: {
|
|
240
|
-
priority?: 'critical' | 'high' | 'medium' | 'low';
|
|
241
|
-
labels?: string[];
|
|
242
|
-
assignee?: string;
|
|
243
|
-
parentId?: string;
|
|
244
|
-
description?: string;
|
|
245
|
-
status?: 'backlog' | 'queue';
|
|
246
|
-
criteria?: Array<{ description: string; command?: string }>;
|
|
247
|
-
},
|
|
248
|
-
): Promise<VcsOp> {
|
|
249
|
-
const id = nextIssueId(rootPath);
|
|
250
|
-
|
|
251
|
-
const op = await createVcsOp('vcs:issueCreate', {
|
|
252
|
-
agentId: ctx.agentId,
|
|
253
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
254
|
-
vcs: {
|
|
255
|
-
issueId: id,
|
|
256
|
-
issueTitle: title,
|
|
257
|
-
issueDescription: opts?.description,
|
|
258
|
-
issueStatus: opts?.status ?? 'backlog',
|
|
259
|
-
issuePriority: opts?.priority ?? 'medium',
|
|
260
|
-
issueLabels: opts?.labels,
|
|
261
|
-
issueAssignee: opts?.assignee,
|
|
262
|
-
parentIssueId: opts?.parentId,
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
ctx.applyOp(op);
|
|
266
|
-
|
|
267
|
-
// Add acceptance criteria if provided
|
|
268
|
-
if (opts?.criteria) {
|
|
269
|
-
for (let i = 0; i < opts.criteria.length; i++) {
|
|
270
|
-
await addCriterion(
|
|
271
|
-
ctx,
|
|
272
|
-
id,
|
|
273
|
-
opts.criteria[i].description,
|
|
274
|
-
opts.criteria[i].command,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return op;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Update an issue's metadata.
|
|
284
|
-
*/
|
|
285
|
-
export async function updateIssue(
|
|
286
|
-
ctx: EngineContext,
|
|
287
|
-
id: string,
|
|
288
|
-
updates: {
|
|
289
|
-
title?: string;
|
|
290
|
-
description?: string;
|
|
291
|
-
priority?: 'critical' | 'high' | 'medium' | 'low';
|
|
292
|
-
labels?: string[];
|
|
293
|
-
assignee?: string;
|
|
294
|
-
status?: 'backlog' | 'queue' | 'in_progress' | 'paused' | 'closed';
|
|
295
|
-
},
|
|
296
|
-
): Promise<VcsOp> {
|
|
297
|
-
const op = await createVcsOp('vcs:issueUpdate', {
|
|
298
|
-
agentId: ctx.agentId,
|
|
299
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
300
|
-
vcs: {
|
|
301
|
-
issueId: id,
|
|
302
|
-
issueTitle: updates.title,
|
|
303
|
-
issueDescription: updates.description,
|
|
304
|
-
issueStatus: updates.status,
|
|
305
|
-
issuePriority: updates.priority,
|
|
306
|
-
issueLabels: updates.labels,
|
|
307
|
-
issueAssignee: updates.assignee,
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
ctx.applyOp(op);
|
|
311
|
-
return op;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Start working on an issue: sets in_progress, auto-assigns, creates branch.
|
|
316
|
-
* Returns the issueStart op. The caller (engine) is responsible for
|
|
317
|
-
* actually creating the branch and switching to it.
|
|
318
|
-
*/
|
|
319
|
-
export async function startIssue(
|
|
320
|
-
ctx: EngineContext,
|
|
321
|
-
id: string,
|
|
322
|
-
branchName: string,
|
|
323
|
-
): Promise<VcsOp> {
|
|
324
|
-
const eid = issueEntityId(id);
|
|
325
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
326
|
-
if (status === 'closed') {
|
|
327
|
-
throw new Error(`Cannot start closed issue ${id}. Reopen it first.`);
|
|
328
|
-
}
|
|
329
|
-
if (status === 'in_progress') {
|
|
330
|
-
throw new Error(`Issue ${id} is already in progress.`);
|
|
331
|
-
}
|
|
332
|
-
// Allow start from backlog, queue, or paused
|
|
333
|
-
|
|
334
|
-
const op = await createVcsOp('vcs:issueStart', {
|
|
335
|
-
agentId: ctx.agentId,
|
|
336
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
337
|
-
vcs: {
|
|
338
|
-
issueId: id,
|
|
339
|
-
issueAssignee: ctx.agentId,
|
|
340
|
-
branchName,
|
|
341
|
-
},
|
|
342
|
-
});
|
|
343
|
-
ctx.applyOp(op);
|
|
344
|
-
return op;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Pause an in-progress issue.
|
|
349
|
-
*/
|
|
350
|
-
export async function pauseIssue(
|
|
351
|
-
ctx: EngineContext,
|
|
352
|
-
id: string,
|
|
353
|
-
note: string,
|
|
354
|
-
): Promise<VcsOp> {
|
|
355
|
-
if (!note || !note.trim()) {
|
|
356
|
-
throw new Error(
|
|
357
|
-
`A pause note is required. Explain why the issue is paused and what must happen before resuming.`,
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const eid = issueEntityId(id);
|
|
362
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
363
|
-
if (status !== 'in_progress') {
|
|
364
|
-
throw new Error(
|
|
365
|
-
`Cannot pause issue ${id} — status is '${status}', expected 'in_progress'.`,
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const op = await createVcsOp('vcs:issuePause', {
|
|
370
|
-
agentId: ctx.agentId,
|
|
371
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
372
|
-
vcs: { issueId: id, pauseNote: note.trim() },
|
|
373
|
-
});
|
|
374
|
-
ctx.applyOp(op);
|
|
375
|
-
return op;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Resume a paused issue.
|
|
380
|
-
*/
|
|
381
|
-
export async function resumeIssue(
|
|
382
|
-
ctx: EngineContext,
|
|
383
|
-
id: string,
|
|
384
|
-
): Promise<VcsOp> {
|
|
385
|
-
const eid = issueEntityId(id);
|
|
386
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
387
|
-
if (status !== 'paused') {
|
|
388
|
-
throw new Error(
|
|
389
|
-
`Cannot resume issue ${id} — status is '${status}', expected 'paused'.`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const op = await createVcsOp('vcs:issueResume', {
|
|
394
|
-
agentId: ctx.agentId,
|
|
395
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
396
|
-
vcs: { issueId: id },
|
|
397
|
-
});
|
|
398
|
-
ctx.applyOp(op);
|
|
399
|
-
return op;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Close an issue. Requires all criteria to have passed and confirm=true.
|
|
404
|
-
*/
|
|
405
|
-
export async function closeIssue(
|
|
406
|
-
ctx: EngineContext,
|
|
407
|
-
id: string,
|
|
408
|
-
opts?: { confirm?: boolean },
|
|
409
|
-
): Promise<{ op?: VcsOp; criteriaResults: CriterionResult[] }> {
|
|
410
|
-
const eid = issueEntityId(id);
|
|
411
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
412
|
-
if (status === 'closed') {
|
|
413
|
-
throw new Error(`Issue ${id} is already closed.`);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Check criteria status in the store
|
|
417
|
-
const criteria = getCriteriaForIssue(ctx, id);
|
|
418
|
-
const results: CriterionResult[] = criteria.map((c) => ({
|
|
419
|
-
id: c.id,
|
|
420
|
-
description: c.description,
|
|
421
|
-
command: c.command,
|
|
422
|
-
status: (c.status as 'passed' | 'failed') ?? ('pending' as any),
|
|
423
|
-
}));
|
|
424
|
-
|
|
425
|
-
const allPassed =
|
|
426
|
-
results.length === 0 || results.every((r) => r.status === 'passed');
|
|
427
|
-
|
|
428
|
-
if (!allPassed) {
|
|
429
|
-
const failing = results.filter((r) => r.status !== 'passed');
|
|
430
|
-
throw new Error(
|
|
431
|
-
`Cannot close issue ${id}: ${failing.length} criteria not passing:\n` +
|
|
432
|
-
failing
|
|
433
|
-
.map((f) => ` - ${f.description ?? f.id} (${f.status})`)
|
|
434
|
-
.join('\n'),
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (!opts?.confirm) {
|
|
439
|
-
return { criteriaResults: results };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Compute duration from startedAt
|
|
443
|
-
const startedAt = getIssueFact(ctx, eid, 'startedAt');
|
|
444
|
-
|
|
445
|
-
const op = await createVcsOp('vcs:issueClose', {
|
|
446
|
-
agentId: ctx.agentId,
|
|
447
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
448
|
-
vcs: { issueId: id },
|
|
449
|
-
});
|
|
450
|
-
ctx.applyOp(op);
|
|
451
|
-
|
|
452
|
-
// Store duration as a fact if we have startedAt
|
|
453
|
-
if (startedAt) {
|
|
454
|
-
const durationMs = Date.now() - new Date(startedAt).getTime();
|
|
455
|
-
ctx.store.addFacts([{ e: eid, a: 'durationMs', v: durationMs }]);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return { op, criteriaResults: results };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Triage a backlog issue to queue (ready to start).
|
|
463
|
-
*/
|
|
464
|
-
export async function triageIssue(
|
|
465
|
-
ctx: EngineContext,
|
|
466
|
-
id: string,
|
|
467
|
-
): Promise<VcsOp> {
|
|
468
|
-
const eid = issueEntityId(id);
|
|
469
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
470
|
-
if (status !== 'backlog') {
|
|
471
|
-
throw new Error(
|
|
472
|
-
`Cannot triage issue ${id} — status is '${status}', expected 'backlog'.`,
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const op = await createVcsOp('vcs:issueUpdate', {
|
|
477
|
-
agentId: ctx.agentId,
|
|
478
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
479
|
-
vcs: {
|
|
480
|
-
issueId: id,
|
|
481
|
-
issueStatus: 'queue',
|
|
482
|
-
},
|
|
483
|
-
});
|
|
484
|
-
ctx.applyOp(op);
|
|
485
|
-
return op;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Reopen a closed issue.
|
|
490
|
-
*/
|
|
491
|
-
export async function reopenIssue(
|
|
492
|
-
ctx: EngineContext,
|
|
493
|
-
id: string,
|
|
494
|
-
): Promise<VcsOp> {
|
|
495
|
-
const eid = issueEntityId(id);
|
|
496
|
-
const status = getIssueFact(ctx, eid, 'status');
|
|
497
|
-
if (status !== 'closed') {
|
|
498
|
-
throw new Error(
|
|
499
|
-
`Cannot reopen issue ${id} — status is '${status}', expected 'closed'.`,
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const op = await createVcsOp('vcs:issueReopen', {
|
|
504
|
-
agentId: ctx.agentId,
|
|
505
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
506
|
-
vcs: { issueId: id },
|
|
507
|
-
});
|
|
508
|
-
ctx.applyOp(op);
|
|
509
|
-
return op;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Assign an issue to an agent.
|
|
514
|
-
*/
|
|
515
|
-
export async function assignIssue(
|
|
516
|
-
ctx: EngineContext,
|
|
517
|
-
id: string,
|
|
518
|
-
agentId: string,
|
|
519
|
-
): Promise<VcsOp> {
|
|
520
|
-
return updateIssue(ctx, id, { assignee: agentId });
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Block an issue by another issue.
|
|
525
|
-
*/
|
|
526
|
-
export async function blockIssue(
|
|
527
|
-
ctx: EngineContext,
|
|
528
|
-
id: string,
|
|
529
|
-
blockedById: string,
|
|
530
|
-
): Promise<VcsOp> {
|
|
531
|
-
const eid = issueEntityId(id);
|
|
532
|
-
const blockerEid = issueEntityId(blockedById);
|
|
533
|
-
|
|
534
|
-
// Validate both issues exist
|
|
535
|
-
if (!getIssueFact(ctx, eid, 'type')) {
|
|
536
|
-
throw new Error(`Issue ${id} not found.`);
|
|
537
|
-
}
|
|
538
|
-
if (!getIssueFact(ctx, blockerEid, 'type')) {
|
|
539
|
-
throw new Error(`Blocking issue ${blockedById} not found.`);
|
|
540
|
-
}
|
|
541
|
-
if (id === blockedById) {
|
|
542
|
-
throw new Error(`Issue cannot block itself.`);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const op = await createVcsOp('vcs:issueBlock', {
|
|
546
|
-
agentId: ctx.agentId,
|
|
547
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
548
|
-
vcs: { issueId: id, blockedByIssueId: blockedById },
|
|
549
|
-
});
|
|
550
|
-
ctx.applyOp(op);
|
|
551
|
-
return op;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Remove a blocking relationship.
|
|
556
|
-
*/
|
|
557
|
-
export async function unblockIssue(
|
|
558
|
-
ctx: EngineContext,
|
|
559
|
-
id: string,
|
|
560
|
-
blockedById: string,
|
|
561
|
-
): Promise<VcsOp> {
|
|
562
|
-
const op = await createVcsOp('vcs:issueUnblock', {
|
|
563
|
-
agentId: ctx.agentId,
|
|
564
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
565
|
-
vcs: { issueId: id, blockedByIssueId: blockedById },
|
|
566
|
-
});
|
|
567
|
-
ctx.applyOp(op);
|
|
568
|
-
return op;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Add an acceptance criterion to an issue.
|
|
573
|
-
*/
|
|
574
|
-
export async function addCriterion(
|
|
575
|
-
ctx: EngineContext,
|
|
576
|
-
issueId: string,
|
|
577
|
-
description: string,
|
|
578
|
-
command?: string,
|
|
579
|
-
): Promise<VcsOp> {
|
|
580
|
-
// Count existing criteria to determine index
|
|
581
|
-
const existing = getCriteriaForIssue(ctx, issueId);
|
|
582
|
-
const index = existing.length + 1;
|
|
583
|
-
const cid = criterionEntityId(issueId, index);
|
|
584
|
-
|
|
585
|
-
const op = await createVcsOp('vcs:criterionAdd', {
|
|
586
|
-
agentId: ctx.agentId,
|
|
587
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
588
|
-
vcs: {
|
|
589
|
-
issueId,
|
|
590
|
-
criterionId: cid,
|
|
591
|
-
criterionDescription: description,
|
|
592
|
-
criterionCommand: command,
|
|
593
|
-
},
|
|
594
|
-
});
|
|
595
|
-
ctx.applyOp(op);
|
|
596
|
-
return op;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Manually set a criterion's status (for non-command criteria).
|
|
601
|
-
*/
|
|
602
|
-
export async function setCriterionStatus(
|
|
603
|
-
ctx: EngineContext,
|
|
604
|
-
issueId: string,
|
|
605
|
-
criterionIndex: number,
|
|
606
|
-
status: 'passed' | 'failed' | 'pending',
|
|
607
|
-
): Promise<VcsOp> {
|
|
608
|
-
const criteria = getCriteriaForIssue(ctx, issueId);
|
|
609
|
-
if (criterionIndex < 1 || criterionIndex > criteria.length) {
|
|
610
|
-
throw new Error(
|
|
611
|
-
`Criterion index ${criterionIndex} out of range (1–${criteria.length})`,
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
const c = criteria[criterionIndex - 1];
|
|
615
|
-
|
|
616
|
-
const op = await createVcsOp('vcs:criterionUpdate', {
|
|
617
|
-
agentId: ctx.agentId,
|
|
618
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
619
|
-
vcs: {
|
|
620
|
-
issueId,
|
|
621
|
-
criterionId: c.id,
|
|
622
|
-
criterionStatus: status,
|
|
623
|
-
},
|
|
624
|
-
});
|
|
625
|
-
ctx.applyOp(op);
|
|
626
|
-
return op;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* Run all acceptance criteria for an issue. Executes test commands
|
|
631
|
-
* and emits criterionUpdate ops with results.
|
|
632
|
-
*/
|
|
633
|
-
export async function runCriteria(
|
|
634
|
-
ctx: EngineContext,
|
|
635
|
-
issueId: string,
|
|
636
|
-
rootPath: string,
|
|
637
|
-
): Promise<CriterionResult[]> {
|
|
638
|
-
const criteria = getCriteriaForIssue(ctx, issueId);
|
|
639
|
-
const results: CriterionResult[] = [];
|
|
640
|
-
|
|
641
|
-
for (const c of criteria) {
|
|
642
|
-
if (!c.command) {
|
|
643
|
-
// No command — check-only criterion, skip automated run
|
|
644
|
-
results.push({
|
|
645
|
-
id: c.id,
|
|
646
|
-
description: c.description,
|
|
647
|
-
status: (c.status as 'passed' | 'failed') ?? 'skipped',
|
|
648
|
-
});
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
let status: 'passed' | 'failed' = 'failed';
|
|
653
|
-
let output = '';
|
|
654
|
-
let exitCode = 1;
|
|
655
|
-
|
|
656
|
-
try {
|
|
657
|
-
const result = await execAsync(c.command, {
|
|
658
|
-
cwd: rootPath,
|
|
659
|
-
timeout: 120_000,
|
|
660
|
-
});
|
|
661
|
-
output = (result.stdout + '\n' + result.stderr).trim();
|
|
662
|
-
exitCode = 0;
|
|
663
|
-
status = 'passed';
|
|
664
|
-
} catch (err: any) {
|
|
665
|
-
output = (err.stdout ?? '') + '\n' + (err.stderr ?? err.message ?? '');
|
|
666
|
-
output = output.trim();
|
|
667
|
-
exitCode = err.code ?? 1;
|
|
668
|
-
status = 'failed';
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Emit criterionUpdate op
|
|
672
|
-
const updateOp = await createVcsOp('vcs:criterionUpdate', {
|
|
673
|
-
agentId: ctx.agentId,
|
|
674
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
675
|
-
vcs: {
|
|
676
|
-
criterionId: c.id,
|
|
677
|
-
criterionStatus: status,
|
|
678
|
-
criterionOutput: output.slice(0, 4096),
|
|
679
|
-
},
|
|
680
|
-
});
|
|
681
|
-
ctx.applyOp(updateOp);
|
|
682
|
-
|
|
683
|
-
results.push({
|
|
684
|
-
id: c.id,
|
|
685
|
-
description: c.description,
|
|
686
|
-
command: c.command,
|
|
687
|
-
status,
|
|
688
|
-
output,
|
|
689
|
-
exitCode,
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
return results;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// ---------------------------------------------------------------------------
|
|
697
|
-
// Queries
|
|
698
|
-
// ---------------------------------------------------------------------------
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* List all issues, optionally filtered.
|
|
702
|
-
*/
|
|
703
|
-
export function listIssues(
|
|
704
|
-
ctx: EngineContext,
|
|
705
|
-
filters?: IssueFilters,
|
|
706
|
-
): IssueInfo[] {
|
|
707
|
-
const issueFacts = ctx.store
|
|
708
|
-
.getFactsByAttribute('type')
|
|
709
|
-
.filter((f) => f.v === 'Issue');
|
|
710
|
-
|
|
711
|
-
let issues = issueFacts.map((f) => buildIssueInfo(ctx, f.e));
|
|
712
|
-
|
|
713
|
-
if (filters?.status) {
|
|
714
|
-
issues = issues.filter((i) => i.status === filters.status);
|
|
715
|
-
}
|
|
716
|
-
if (filters?.assignee) {
|
|
717
|
-
issues = issues.filter((i) => i.assignee === filters.assignee);
|
|
718
|
-
}
|
|
719
|
-
if (filters?.label) {
|
|
720
|
-
issues = issues.filter((i) => i.labels.includes(filters.label!));
|
|
721
|
-
}
|
|
722
|
-
if (filters?.parentId) {
|
|
723
|
-
issues = issues.filter((i) => i.parentId === filters.parentId);
|
|
724
|
-
}
|
|
725
|
-
if (filters?.blocked !== undefined) {
|
|
726
|
-
issues = issues.filter((i) => i.isBlocked === filters.blocked);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
return issues;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Get a single issue by ID.
|
|
734
|
-
*/
|
|
735
|
-
export function getIssue(ctx: EngineContext, id: string): IssueInfo | null {
|
|
736
|
-
const eid = issueEntityId(id);
|
|
737
|
-
const typeFact = ctx.store
|
|
738
|
-
.getFactsByEntity(eid)
|
|
739
|
-
.find((f) => f.a === 'type' && f.v === 'Issue');
|
|
740
|
-
if (!typeFact) return null;
|
|
741
|
-
return buildIssueInfo(ctx, eid);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Get all active (in_progress) issues.
|
|
746
|
-
*/
|
|
747
|
-
export function getActiveIssues(ctx: EngineContext): IssueInfo[] {
|
|
748
|
-
return listIssues(ctx, { status: 'in_progress' });
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// ---------------------------------------------------------------------------
|
|
752
|
-
// Completion Readiness
|
|
753
|
-
// ---------------------------------------------------------------------------
|
|
754
|
-
|
|
755
|
-
export interface CompletionReadiness {
|
|
756
|
-
ready: boolean;
|
|
757
|
-
queue: IssueInfo[];
|
|
758
|
-
paused: IssueInfo[];
|
|
759
|
-
inProgress: IssueInfo[];
|
|
760
|
-
summary: string;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Check whether all work is complete: no issues in queue, paused, or in_progress.
|
|
765
|
-
*/
|
|
766
|
-
export function checkCompletionReadiness(
|
|
767
|
-
ctx: EngineContext,
|
|
768
|
-
): CompletionReadiness {
|
|
769
|
-
const all = listIssues(ctx);
|
|
770
|
-
const queue = all.filter((i) => i.status === 'queue');
|
|
771
|
-
const paused = all.filter((i) => i.status === 'paused');
|
|
772
|
-
const inProgress = all.filter((i) => i.status === 'in_progress');
|
|
773
|
-
|
|
774
|
-
const ready =
|
|
775
|
-
queue.length === 0 && paused.length === 0 && inProgress.length === 0;
|
|
776
|
-
|
|
777
|
-
const parts: string[] = [];
|
|
778
|
-
if (ready) {
|
|
779
|
-
parts.push('✓ All clear — no queue, paused, or in-progress issues.');
|
|
780
|
-
} else {
|
|
781
|
-
parts.push('✗ Not ready for completion:');
|
|
782
|
-
if (queue.length > 0) {
|
|
783
|
-
parts.push(
|
|
784
|
-
` Queue (${queue.length}): ${queue.map((i) => i.id).join(', ')}`,
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
if (inProgress.length > 0) {
|
|
788
|
-
parts.push(
|
|
789
|
-
` In progress (${inProgress.length}): ${inProgress.map((i) => i.id).join(', ')}`,
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
|
-
if (paused.length > 0) {
|
|
793
|
-
parts.push(
|
|
794
|
-
` Paused (${paused.length}): ${paused.map((i) => `${i.id}${i.pauseNote ? ` — ${i.pauseNote}` : ''}`).join(', ')}`,
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
return { ready, queue, paused, inProgress, summary: parts.join('\n') };
|
|
800
|
-
}
|