trellis 2.0.13 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
package/src/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
- }