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/mcp/server.ts DELETED
@@ -1,1278 +0,0 @@
1
- /**
2
- * TrellisVCS MCP Server
3
- *
4
- * @module mcp
5
- *
6
- * Exposes TrellisVcsEngine as an MCP (Model Context Protocol) server,
7
- * enabling any MCP-compatible AI agent to interact with TrellisVCS
8
- * repositories through structured tool calls and resource queries.
9
- *
10
- * Tools provide write/query actions (status, log, milestone, branch, etc.).
11
- * Resources provide read-only context (op stream, file list, garden clusters).
12
- */
13
-
14
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
- import { z } from 'zod';
16
- import { resolve } from 'path';
17
- import { TrellisVcsEngine } from '../engine.js';
18
- import { HookRegistry } from '../decisions/index.js';
19
- import { wrapToolHandler } from '../decisions/auto-capture.js';
20
- import type { DecisionRecorder } from '../decisions/auto-capture.js';
21
-
22
- // ---------------------------------------------------------------------------
23
- // Helpers
24
- // ---------------------------------------------------------------------------
25
-
26
- function getEngine(rootPath: string): TrellisVcsEngine {
27
- const absPath = resolve(rootPath);
28
- if (!TrellisVcsEngine.isRepo(absPath)) {
29
- throw new Error(`Not a TrellisVCS repository: ${absPath}`);
30
- }
31
- const engine = new TrellisVcsEngine({ rootPath: absPath });
32
- engine.open();
33
- return engine;
34
- }
35
-
36
- function text(content: string) {
37
- return { content: [{ type: 'text' as const, text: content }] };
38
- }
39
-
40
- // ---------------------------------------------------------------------------
41
- // Server
42
- // ---------------------------------------------------------------------------
43
-
44
- export function createTrellisMcpServer(): McpServer {
45
- const server = new McpServer({
46
- name: 'trellis-vcs',
47
- version: '0.1.0',
48
- });
49
-
50
- // -----------------------------------------------------------------------
51
- // Tool: trellis_status
52
- // -----------------------------------------------------------------------
53
- server.registerTool(
54
- 'trellis_status',
55
- {
56
- description:
57
- 'Get the current status of a TrellisVCS repository: branch, op count, tracked files, and recent activity.',
58
- inputSchema: {
59
- path: z
60
- .string()
61
- .default('.')
62
- .describe('Path to the TrellisVCS repository root'),
63
- },
64
- },
65
- async ({ path }) => {
66
- const engine = getEngine(path);
67
- const s = engine.status();
68
- const lines = [
69
- `Branch: ${s.branch}`,
70
- `Total ops: ${s.totalOps}`,
71
- `Tracked files: ${s.trackedFiles}`,
72
- '',
73
- s.lastOp
74
- ? `Last op: ${s.lastOp.kind} at ${s.lastOp.timestamp}`
75
- : 'No ops yet.',
76
- '',
77
- 'Recent activity:',
78
- ...s.recentOps
79
- .slice(-5)
80
- .map(
81
- (op) => ` ${op.kind} ${op.vcs?.filePath ?? ''} ${op.timestamp}`,
82
- ),
83
- ];
84
- return text(lines.join('\n'));
85
- },
86
- );
87
-
88
- // -----------------------------------------------------------------------
89
- // Tool: trellis_log
90
- // -----------------------------------------------------------------------
91
- server.registerTool(
92
- 'trellis_log',
93
- {
94
- description:
95
- 'Show operation history from the causal stream. Optionally filter by file path or limit results.',
96
- inputSchema: {
97
- path: z.string().default('.').describe('Repository root path'),
98
- limit: z
99
- .number()
100
- .optional()
101
- .describe('Maximum number of ops to return (default: 20)'),
102
- filePath: z
103
- .string()
104
- .optional()
105
- .describe('Filter ops by affected file path'),
106
- },
107
- },
108
- async ({ path, limit, filePath }) => {
109
- const engine = getEngine(path);
110
- const ops = engine.log({ limit: limit ?? 20, filePath });
111
- const lines = ops.map(
112
- (op) =>
113
- `[${op.timestamp}] ${op.kind} ${op.vcs?.filePath ?? ''} (${op.hash.slice(0, 20)}…)`,
114
- );
115
- return text(lines.length > 0 ? lines.join('\n') : 'No ops found.');
116
- },
117
- );
118
-
119
- // -----------------------------------------------------------------------
120
- // Tool: trellis_files
121
- // -----------------------------------------------------------------------
122
- server.registerTool(
123
- 'trellis_files',
124
- {
125
- description: 'List all tracked files in the TrellisVCS repository.',
126
- inputSchema: {
127
- path: z.string().default('.').describe('Repository root path'),
128
- },
129
- },
130
- async ({ path }) => {
131
- const engine = getEngine(path);
132
- const files = engine.trackedFiles();
133
- const lines = files.map(
134
- (f) =>
135
- `${f.path}${f.contentHash ? ` (${f.contentHash.slice(0, 12)}…)` : ''}`,
136
- );
137
- return text(
138
- lines.length > 0
139
- ? `Tracked files (${lines.length}):\n${lines.join('\n')}`
140
- : 'No tracked files.',
141
- );
142
- },
143
- );
144
-
145
- // -----------------------------------------------------------------------
146
- // Tool: trellis_branch
147
- // -----------------------------------------------------------------------
148
- server.registerTool(
149
- 'trellis_branch',
150
- {
151
- description:
152
- 'List, create, switch, or delete branches. Action defaults to "list".',
153
- inputSchema: {
154
- path: z.string().default('.').describe('Repository root path'),
155
- action: z
156
- .enum(['list', 'create', 'switch', 'delete'])
157
- .default('list')
158
- .describe('Branch action to perform'),
159
- name: z
160
- .string()
161
- .optional()
162
- .describe('Branch name (required for create/switch/delete)'),
163
- },
164
- },
165
- async ({ path, action, name }) => {
166
- const engine = getEngine(path);
167
-
168
- if (action === 'list') {
169
- const branches = engine.listBranches();
170
- const lines = branches.map(
171
- (b) => `${b.isCurrent ? '* ' : ' '}${b.name}`,
172
- );
173
- return text(lines.join('\n'));
174
- }
175
-
176
- if (!name) {
177
- return text(`Error: branch name is required for "${action}".`);
178
- }
179
-
180
- if (action === 'create') {
181
- const op = await engine.createBranch(name);
182
- return text(`Created branch "${name}" (op: ${op.hash.slice(0, 20)}…)`);
183
- }
184
- if (action === 'switch') {
185
- engine.switchBranch(name);
186
- return text(`Switched to branch "${name}".`);
187
- }
188
- if (action === 'delete') {
189
- const op = await engine.deleteBranch(name);
190
- return text(`Deleted branch "${name}" (op: ${op.hash.slice(0, 20)}…)`);
191
- }
192
-
193
- return text(`Unknown action: ${action}`);
194
- },
195
- );
196
-
197
- // -----------------------------------------------------------------------
198
- // Tool: trellis_milestone
199
- // -----------------------------------------------------------------------
200
- server.registerTool(
201
- 'trellis_milestone',
202
- {
203
- description:
204
- 'Create or list milestones. Milestones are narrative checkpoints in the causal stream.',
205
- inputSchema: {
206
- path: z.string().default('.').describe('Repository root path'),
207
- action: z
208
- .enum(['list', 'create'])
209
- .default('list')
210
- .describe('Milestone action'),
211
- message: z
212
- .string()
213
- .optional()
214
- .describe('Milestone message (required for create)'),
215
- fromOpHash: z
216
- .string()
217
- .optional()
218
- .describe('Start of op range (auto-detected if omitted)'),
219
- toOpHash: z
220
- .string()
221
- .optional()
222
- .describe('End of op range (auto-detected if omitted)'),
223
- },
224
- },
225
- async ({ path, action, message, fromOpHash, toOpHash }) => {
226
- const engine = getEngine(path);
227
-
228
- if (action === 'list') {
229
- const milestones = engine.listMilestones();
230
- if (milestones.length === 0) return text('No milestones yet.');
231
- const lines = milestones.map(
232
- (m) =>
233
- `★ ${m.message} (${m.id.slice(0, 24)}…) — ${m.affectedFiles.length} files`,
234
- );
235
- return text(`Milestones (${milestones.length}):\n${lines.join('\n')}`);
236
- }
237
-
238
- if (!message) {
239
- return text('Error: message is required for creating a milestone.');
240
- }
241
-
242
- const op = await engine.createMilestone(message, {
243
- fromOpHash,
244
- toOpHash,
245
- });
246
- return text(
247
- `Created milestone "${message}" (op: ${op.hash.slice(0, 20)}…)`,
248
- );
249
- },
250
- );
251
-
252
- // -----------------------------------------------------------------------
253
- // Tool: trellis_diff
254
- // -----------------------------------------------------------------------
255
- server.registerTool(
256
- 'trellis_diff',
257
- {
258
- description:
259
- 'Show file-level diff between two points in the causal stream.',
260
- inputSchema: {
261
- path: z.string().default('.').describe('Repository root path'),
262
- fromHash: z.string().describe('Starting op hash'),
263
- toHash: z.string().describe('Ending op hash'),
264
- },
265
- },
266
- async ({ path, fromHash, toHash }) => {
267
- const engine = getEngine(path);
268
- const result = engine.diffOps(fromHash, toHash);
269
- if (result.diffs.length === 0) return text('No changes.');
270
- const lines = result.diffs.map(
271
- (d) =>
272
- `${d.kind}: ${d.path}${d.unifiedDiff ? '\n' + d.unifiedDiff : ''}`,
273
- );
274
- return text(lines.join('\n\n'));
275
- },
276
- );
277
-
278
- // -----------------------------------------------------------------------
279
- // Tool: trellis_garden
280
- // -----------------------------------------------------------------------
281
- server.registerTool(
282
- 'trellis_garden',
283
- {
284
- description:
285
- 'Explore the Idea Garden: list abandoned work clusters, search by keyword/file, view stats, or revive a cluster into a new branch.',
286
- inputSchema: {
287
- path: z.string().default('.').describe('Repository root path'),
288
- action: z
289
- .enum(['list', 'search', 'stats', 'revive'])
290
- .default('list')
291
- .describe('Garden action'),
292
- keyword: z
293
- .string()
294
- .optional()
295
- .describe('Search keyword (for search action)'),
296
- file: z
297
- .string()
298
- .optional()
299
- .describe('Filter by file path (for list/search)'),
300
- status: z
301
- .enum(['abandoned', 'draft', 'revived'])
302
- .optional()
303
- .describe('Filter by cluster status'),
304
- clusterId: z
305
- .string()
306
- .optional()
307
- .describe('Cluster ID (required for revive)'),
308
- limit: z.number().optional().describe('Max results (default: 10)'),
309
- },
310
- },
311
- async ({ path, action, keyword, file, status, clusterId, limit }) => {
312
- const engine = getEngine(path);
313
- const garden = engine.garden();
314
-
315
- if (action === 'stats') {
316
- const s = garden.stats();
317
- return text(
318
- [
319
- `Idea Garden Stats:`,
320
- ` Total clusters: ${s.total}`,
321
- ` Abandoned: ${s.abandoned}`,
322
- ` Draft: ${s.draft}`,
323
- ` Revived: ${s.revived}`,
324
- ` Total ops: ${s.totalOps}`,
325
- ` Affected files: ${s.totalFiles}`,
326
- ].join('\n'),
327
- );
328
- }
329
-
330
- if (action === 'revive') {
331
- if (!clusterId) return text('Error: clusterId is required for revive.');
332
- const result = garden.revive(clusterId);
333
- return text(
334
- `Revived cluster "${clusterId}" → branch "${result.branchName}" (${result.ops.length} ops)`,
335
- );
336
- }
337
-
338
- if (action === 'search' || action === 'list') {
339
- const clusters = garden.search({
340
- keyword,
341
- file,
342
- status,
343
- limit: limit ?? 10,
344
- });
345
- if (clusters.length === 0) return text('No idea clusters found.');
346
- const lines = clusters.map(
347
- (c) =>
348
- `[${c.status}] ${c.id.slice(0, 20)}… — ${c.ops.length} ops, ${c.affectedFiles.length} files\n Intent: ${c.estimatedIntent}`,
349
- );
350
- return text(`Idea clusters (${clusters.length}):\n${lines.join('\n')}`);
351
- }
352
-
353
- return text(`Unknown garden action: ${action}`);
354
- },
355
- );
356
-
357
- // -----------------------------------------------------------------------
358
- // Tool: trellis_parse
359
- // -----------------------------------------------------------------------
360
- server.registerTool(
361
- 'trellis_parse',
362
- {
363
- description:
364
- 'Parse a TypeScript/JavaScript file into AST-level entities (functions, classes, interfaces, imports).',
365
- inputSchema: {
366
- content: z.string().describe('File content to parse'),
367
- filePath: z
368
- .string()
369
- .describe(
370
- 'File path (used for language detection, e.g. "src/auth.ts")',
371
- ),
372
- },
373
- },
374
- async ({ content, filePath }) => {
375
- const engine = getEngine('.');
376
- const result = engine.parseFile(content, filePath);
377
- if (!result)
378
- return text(
379
- `No parser available for "${filePath}". Currently supports .ts, .js, .tsx, .jsx.`,
380
- );
381
- const lines = [
382
- `Declarations (${result.declarations.length}):`,
383
- ...result.declarations.map(
384
- (d) => ` ${d.kind} ${d.name} (${d.id.slice(0, 16)}…)`,
385
- ),
386
- '',
387
- `Imports (${result.imports.length}):`,
388
- ...result.imports.map(
389
- (i) => ` ${i.specifiers.join(', ')} from "${i.source}"`,
390
- ),
391
- '',
392
- `Exports (${result.exports.length}):`,
393
- ...result.exports.map(
394
- (e) =>
395
- ` ${e.specifiers.join(', ')}${e.source ? ` from "${e.source}"` : ''}`,
396
- ),
397
- ];
398
- return text(lines.join('\n'));
399
- },
400
- );
401
-
402
- // -----------------------------------------------------------------------
403
- // Tool: trellis_semantic_diff
404
- // -----------------------------------------------------------------------
405
- server.registerTool(
406
- 'trellis_semantic_diff',
407
- {
408
- description:
409
- 'Compute a semantic diff between two versions of a TypeScript/JavaScript file. Returns structured AST-level patches (symbolAdd, symbolRemove, symbolModify, symbolRename, importAdd, etc.).',
410
- inputSchema: {
411
- oldContent: z.string().describe('Old file content'),
412
- newContent: z.string().describe('New file content'),
413
- filePath: z.string().describe('File path for language detection'),
414
- },
415
- },
416
- async ({ oldContent, newContent, filePath }) => {
417
- const engine = getEngine('.');
418
- const patches = engine.semanticDiff(oldContent, newContent, filePath);
419
- if (patches.length === 0) return text('No semantic changes detected.');
420
- const lines = patches.map((p) => {
421
- const base = `${p.kind}: ${(p as any).entityId ?? (p as any).source ?? ''}`;
422
- if ('newName' in p) return `${base} → ${(p as any).newName}`;
423
- return base;
424
- });
425
- return text(`Semantic patches (${patches.length}):\n${lines.join('\n')}`);
426
- },
427
- );
428
-
429
- // -----------------------------------------------------------------------
430
- // Tool: trellis_init
431
- // -----------------------------------------------------------------------
432
- server.registerTool(
433
- 'trellis_init',
434
- {
435
- description:
436
- 'Initialize a new TrellisVCS repository. Scans the directory and creates initial ops for all existing files.',
437
- inputSchema: {
438
- path: z
439
- .string()
440
- .default('.')
441
- .describe('Directory to initialize as a TrellisVCS repo'),
442
- },
443
- },
444
- async ({ path }) => {
445
- const absPath = resolve(path);
446
- if (TrellisVcsEngine.isRepo(absPath)) {
447
- return text(`Already a TrellisVCS repository: ${absPath}`);
448
- }
449
- const engine = new TrellisVcsEngine({ rootPath: absPath });
450
- const result = await engine.initRepo();
451
- return text(
452
- `Initialized TrellisVCS repository at ${absPath}\nOps created: ${result.opsCreated}`,
453
- );
454
- },
455
- );
456
-
457
- // -----------------------------------------------------------------------
458
- // Tool: trellis_issue_create
459
- // -----------------------------------------------------------------------
460
- server.registerTool(
461
- 'trellis_issue_create',
462
- {
463
- description:
464
- 'Create a new issue with title, priority, labels, acceptance criteria, and optional parent for sub-tasks.',
465
- inputSchema: {
466
- path: z.string().default('.').describe('Repository path'),
467
- title: z.string().describe('Issue title'),
468
- description: z.string().optional().describe('Short issue description'),
469
- status: z
470
- .enum(['backlog', 'queue'])
471
- .default('backlog')
472
- .describe('Initial status (defaults to backlog)'),
473
- priority: z
474
- .enum(['critical', 'high', 'medium', 'low'])
475
- .default('medium')
476
- .describe('Issue priority'),
477
- labels: z.string().optional().describe('Comma-separated labels'),
478
- assignee: z.string().optional().describe('Agent ID to assign'),
479
- parentId: z
480
- .string()
481
- .optional()
482
- .describe('Parent issue ID for sub-tasks (e.g. TRL-1)'),
483
- criteria: z
484
- .array(
485
- z.object({
486
- description: z.string(),
487
- command: z.string().optional(),
488
- }),
489
- )
490
- .optional()
491
- .describe('Acceptance criteria with optional test commands'),
492
- },
493
- },
494
- async ({
495
- path,
496
- title,
497
- description,
498
- status,
499
- priority,
500
- labels,
501
- assignee,
502
- parentId,
503
- criteria,
504
- }) => {
505
- const engine = getEngine(path);
506
- const parsedLabels = labels
507
- ? labels.split(',').map((l: string) => l.trim())
508
- : undefined;
509
- const op = await engine.createIssue(title, {
510
- description,
511
- status,
512
- priority,
513
- labels: parsedLabels,
514
- assignee,
515
- parentId,
516
- criteria,
517
- });
518
- const issue = engine.getIssue(op.vcs?.issueId ?? '');
519
- return text(
520
- `Issue created: ${op.vcs?.issueId}\n` +
521
- `Title: ${title}\nPriority: ${priority}\n` +
522
- (parsedLabels ? `Labels: ${parsedLabels.join(', ')}\n` : '') +
523
- (parentId ? `Parent: ${parentId}\n` : '') +
524
- (criteria ? `Criteria: ${criteria.length}\n` : ''),
525
- );
526
- },
527
- );
528
-
529
- // -----------------------------------------------------------------------
530
- // Tool: trellis_issue_list
531
- // -----------------------------------------------------------------------
532
- server.registerTool(
533
- 'trellis_issue_list',
534
- {
535
- description:
536
- 'List issues with optional filters by status, assignee, label, or parent.',
537
- inputSchema: {
538
- path: z.string().default('.').describe('Repository path'),
539
- status: z
540
- .enum(['backlog', 'queue', 'in_progress', 'paused', 'closed'])
541
- .optional()
542
- .describe('Filter by status'),
543
- assignee: z.string().optional().describe('Filter by assignee'),
544
- label: z.string().optional().describe('Filter by label'),
545
- parentId: z.string().optional().describe('Filter by parent issue ID'),
546
- },
547
- },
548
- async ({ path, status, assignee, label, parentId }) => {
549
- const engine = getEngine(path);
550
- const issues = engine.listIssues({ status, assignee, label, parentId });
551
- if (issues.length === 0) return text('No issues found.');
552
- const lines = issues.map(
553
- (i) =>
554
- `[${i.status}] ${i.id} — ${i.title ?? '(untitled)'}` +
555
- (i.priority ? ` (${i.priority})` : '') +
556
- (i.assignee ? ` → ${i.assignee}` : '') +
557
- (i.criteria.length > 0
558
- ? ` (${i.criteria.filter((c) => c.status === 'passed').length}/${i.criteria.length} AC)`
559
- : ''),
560
- );
561
- return text(`Issues (${issues.length}):\n${lines.join('\n')}`);
562
- },
563
- );
564
-
565
- // -----------------------------------------------------------------------
566
- // Tool: trellis_issue_triage
567
- // -----------------------------------------------------------------------
568
- server.registerTool(
569
- 'trellis_issue_triage',
570
- {
571
- description:
572
- 'Move an issue from backlog to queue status (ready for work).',
573
- inputSchema: {
574
- path: z.string().default('.').describe('Repository path'),
575
- id: z.string().describe('Issue ID (e.g. TRL-1)'),
576
- },
577
- },
578
- async ({ path, id }) => {
579
- const engine = getEngine(path);
580
- await engine.triageIssue(id);
581
- return text(`Triaged issue ${id} → queue`);
582
- },
583
- );
584
-
585
- // -----------------------------------------------------------------------
586
- // Tool: trellis_issue_update
587
- // -----------------------------------------------------------------------
588
- server.registerTool(
589
- 'trellis_issue_update',
590
- {
591
- description:
592
- 'Update issue metadata: title, description, status, priority, labels, or assignee.',
593
- inputSchema: {
594
- path: z.string().default('.').describe('Repository path'),
595
- id: z.string().describe('Issue ID (e.g. TRL-1)'),
596
- title: z.string().optional().describe('New title'),
597
- description: z.string().optional().describe('Short description'),
598
- status: z
599
- .enum(['backlog', 'queue', 'in_progress', 'paused', 'closed'])
600
- .optional()
601
- .describe('New status'),
602
- priority: z
603
- .enum(['critical', 'high', 'medium', 'low'])
604
- .optional()
605
- .describe('New priority'),
606
- labels: z.string().optional().describe('Comma-separated labels'),
607
- assignee: z.string().optional().describe('Agent to assign'),
608
- },
609
- },
610
- async ({
611
- path,
612
- id,
613
- title,
614
- description,
615
- status,
616
- priority,
617
- labels,
618
- assignee,
619
- }) => {
620
- const engine = getEngine(path);
621
- const updates: Record<string, any> = {};
622
- if (title !== undefined) updates.title = title;
623
- if (description !== undefined) updates.description = description;
624
- if (status !== undefined) updates.status = status;
625
- if (priority !== undefined) updates.priority = priority;
626
- if (labels !== undefined)
627
- updates.labels = labels.split(',').map((l: string) => l.trim());
628
- if (assignee !== undefined) updates.assignee = assignee;
629
- await engine.updateIssue(id, updates);
630
- return text(`Updated issue ${id}`);
631
- },
632
- );
633
-
634
- // -----------------------------------------------------------------------
635
- // Tool: trellis_issue_start
636
- // -----------------------------------------------------------------------
637
- server.registerTool(
638
- 'trellis_issue_start',
639
- {
640
- description:
641
- 'Start working on an issue. Auto-creates a feature branch and assigns the current agent.',
642
- inputSchema: {
643
- path: z.string().default('.').describe('Repository path'),
644
- id: z.string().describe('Issue ID (e.g. TRL-1)'),
645
- },
646
- },
647
- async ({ path, id }) => {
648
- const engine = getEngine(path);
649
- await engine.startIssue(id);
650
- const issue = engine.getIssue(id);
651
- return text(
652
- `Started issue ${id}\n` +
653
- (issue?.branchName ? `Branch: ${issue.branchName}\n` : '') +
654
- (issue?.assignee ? `Assignee: ${issue.assignee}\n` : ''),
655
- );
656
- },
657
- );
658
-
659
- // -----------------------------------------------------------------------
660
- // Tool: trellis_issue_pause
661
- // -----------------------------------------------------------------------
662
- server.registerTool(
663
- 'trellis_issue_pause',
664
- {
665
- description:
666
- 'Pause an in-progress issue and switch back to the default branch. Requires a note explaining why and what must happen before resuming.',
667
- inputSchema: {
668
- path: z.string().default('.').describe('Repository path'),
669
- id: z.string().describe('Issue ID'),
670
- note: z
671
- .string()
672
- .describe('Why paused and what must happen before resuming'),
673
- },
674
- },
675
- async ({ path, id, note }) => {
676
- const engine = getEngine(path);
677
- await engine.pauseIssue(id, note);
678
- return text(
679
- `Paused issue ${id}\nNote: ${note}\nSwitched to: ${engine.getCurrentBranch()}`,
680
- );
681
- },
682
- );
683
-
684
- // -----------------------------------------------------------------------
685
- // Tool: trellis_issue_resume
686
- // -----------------------------------------------------------------------
687
- server.registerTool(
688
- 'trellis_issue_resume',
689
- {
690
- description:
691
- 'Resume a paused issue and switch back to its feature branch.',
692
- inputSchema: {
693
- path: z.string().default('.').describe('Repository path'),
694
- id: z.string().describe('Issue ID'),
695
- },
696
- },
697
- async ({ path, id }) => {
698
- const engine = getEngine(path);
699
- await engine.resumeIssue(id);
700
- const issue = engine.getIssue(id);
701
- return text(
702
- `Resumed issue ${id}\n` +
703
- (issue?.branchName ? `Branch: ${issue.branchName}\n` : ''),
704
- );
705
- },
706
- );
707
-
708
- // -----------------------------------------------------------------------
709
- // Tool: trellis_issue_check
710
- // -----------------------------------------------------------------------
711
- server.registerTool(
712
- 'trellis_issue_check',
713
- {
714
- description:
715
- 'Run all acceptance criteria for an issue. Executes test commands and reports pass/fail status.',
716
- inputSchema: {
717
- path: z.string().default('.').describe('Repository path'),
718
- id: z.string().describe('Issue ID'),
719
- },
720
- },
721
- async ({ path, id }) => {
722
- const engine = getEngine(path);
723
- const results = await engine.runCriteria(id);
724
- if (results.length === 0) return text('No acceptance criteria defined.');
725
- const lines = results.map(
726
- (r) =>
727
- `[${r.status.toUpperCase()}] ${r.description ?? r.id}` +
728
- (r.command ? ` ($ ${r.command})` : '') +
729
- (r.status === 'failed' && r.output
730
- ? `\n ${r.output.split('\n')[0]}`
731
- : ''),
732
- );
733
- const passed = results.filter((r) => r.status === 'passed').length;
734
- return text(
735
- `Criteria results (${passed}/${results.length} passed):\n${lines.join('\n')}`,
736
- );
737
- },
738
- );
739
-
740
- // -----------------------------------------------------------------------
741
- // Tool: trellis_issue_close
742
- // -----------------------------------------------------------------------
743
- server.registerTool(
744
- 'trellis_issue_close',
745
- {
746
- description:
747
- 'Close an issue. All acceptance criteria must be passing. Requires confirm=true.',
748
- inputSchema: {
749
- path: z.string().default('.').describe('Repository path'),
750
- id: z.string().describe('Issue ID'),
751
- confirm: z
752
- .boolean()
753
- .default(false)
754
- .describe('Must be true to actually close the issue'),
755
- },
756
- },
757
- async ({ path, id, confirm }) => {
758
- const engine = getEngine(path);
759
- try {
760
- const result = await engine.closeIssue(id, { confirm });
761
- if (!result.op) {
762
- const lines = result.criteriaResults.map(
763
- (r) => `[${r.status}] ${r.description ?? r.id}`,
764
- );
765
- return text(
766
- `Criteria status for ${id}:\n${lines.join('\n')}\n\nAll criteria pass. Set confirm=true to close.`,
767
- );
768
- }
769
- return text(`Issue ${id} closed.`);
770
- } catch (err: any) {
771
- return text(`Error: ${err.message}`);
772
- }
773
- },
774
- );
775
-
776
- // ── trellis_issue_block ──────────────────────────────────────────────
777
- server.tool(
778
- 'trellis_issue_block',
779
- 'Mark an issue as blocked by another issue',
780
- {
781
- id: { type: 'string', description: 'Issue ID to block (e.g. TRL-1)' },
782
- blockedBy: {
783
- type: 'string',
784
- description: 'Issue ID that blocks it (e.g. TRL-2)',
785
- },
786
- },
787
- async ({ id, blockedBy }) => {
788
- try {
789
- await engine.blockIssue(id as string, blockedBy as string);
790
- return {
791
- content: [
792
- {
793
- type: 'text',
794
- text: `Issue ${id} is now blocked by ${blockedBy}`,
795
- },
796
- ],
797
- };
798
- } catch (err: any) {
799
- return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
800
- }
801
- },
802
- );
803
-
804
- // ── trellis_issue_unblock ────────────────────────────────────────────
805
- server.tool(
806
- 'trellis_issue_unblock',
807
- 'Remove a blocking relationship between issues',
808
- {
809
- id: { type: 'string', description: 'Blocked issue ID (e.g. TRL-1)' },
810
- blockedBy: {
811
- type: 'string',
812
- description: 'Blocking issue ID to remove (e.g. TRL-2)',
813
- },
814
- },
815
- async ({ id, blockedBy }) => {
816
- try {
817
- await engine.unblockIssue(id as string, blockedBy as string);
818
- return {
819
- content: [
820
- {
821
- type: 'text',
822
- text: `Issue ${id} is no longer blocked by ${blockedBy}`,
823
- },
824
- ],
825
- };
826
- } catch (err: any) {
827
- return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
828
- }
829
- },
830
- );
831
-
832
- // -----------------------------------------------------------------------
833
- // Tool: trellis_issue_readiness
834
- // -----------------------------------------------------------------------
835
- server.registerTool(
836
- 'trellis_issue_readiness',
837
- {
838
- description:
839
- 'Check completion readiness: verifies no issues remain in queue, paused, or in-progress status.',
840
- inputSchema: {
841
- path: z.string().default('.').describe('Repository path'),
842
- },
843
- },
844
- async ({ path }) => {
845
- const engine = getEngine(path);
846
- const result = engine.checkCompletionReadiness();
847
- return text(result.summary);
848
- },
849
- );
850
-
851
- // -----------------------------------------------------------------------
852
- // Tool: trellis_refs
853
- // -----------------------------------------------------------------------
854
- server.registerTool(
855
- 'trellis_refs',
856
- {
857
- description:
858
- 'Query wiki-link [[...]] references: list outgoing refs from a file, find backlinks to an entity, or get index stats.',
859
- inputSchema: {
860
- path: z.string().default('.').describe('Repository root path'),
861
- action: z
862
- .enum(['outgoing', 'backlinks', 'stats'])
863
- .default('stats')
864
- .describe('Query action'),
865
- file: z
866
- .string()
867
- .optional()
868
- .describe('File path for outgoing refs (required for "outgoing")'),
869
- entity: z
870
- .string()
871
- .optional()
872
- .describe(
873
- 'Entity target for backlinks (e.g. "TRL-5", "src/engine.ts")',
874
- ),
875
- },
876
- },
877
- async ({ path, action, file, entity }) => {
878
- const engine = getEngine(path);
879
- const { readFileSync } = require('fs');
880
- const { join } = require('path');
881
- const {
882
- buildRefIndex,
883
- getOutgoingRefs,
884
- getBacklinks,
885
- getIndexStats,
886
- createResolverContext,
887
- resolveRef,
888
- } = require('../links/index.js');
889
-
890
- const resolverCtx = createResolverContext(engine);
891
- const rootPath = engine.getRootPath();
892
-
893
- // Build index from all tracked files
894
- const trackedFiles = engine.trackedFiles();
895
- const fileContents: Array<{ path: string; content: string }> = [];
896
- for (const f of trackedFiles) {
897
- try {
898
- const absPath = join(rootPath, f.path);
899
- const content = readFileSync(absPath, 'utf-8');
900
- fileContents.push({ path: f.path, content });
901
- } catch {}
902
- }
903
- const index = buildRefIndex(fileContents, resolverCtx);
904
-
905
- if (action === 'stats') {
906
- const stats = getIndexStats(index);
907
- return text(
908
- [
909
- `Reference Index Stats:`,
910
- ` Files with refs: ${stats.totalFiles}`,
911
- ` Total refs: ${stats.totalRefs}`,
912
- ` Unique entities: ${stats.totalEntities}`,
913
- ].join('\n'),
914
- );
915
- }
916
-
917
- if (action === 'outgoing') {
918
- if (!file)
919
- return text('Error: file parameter is required for outgoing refs.');
920
- const refs = getOutgoingRefs(index, file);
921
- if (refs.length === 0)
922
- return text(`No [[...]] references found in: ${file}`);
923
- const lines = refs.map((ref: any) => {
924
- const resolved = resolveRef(ref, resolverCtx);
925
- return `[${resolved.state}] [[${ref.raw}]] → ${resolved.entityId ?? 'unresolved'} (L${ref.source.line})`;
926
- });
927
- return text(
928
- `References in ${file} (${refs.length}):\n${lines.join('\n')}`,
929
- );
930
- }
931
-
932
- if (action === 'backlinks') {
933
- if (!entity)
934
- return text('Error: entity parameter is required for backlinks.');
935
- const candidates = [
936
- `issue:${entity}`,
937
- `file:${entity}`,
938
- `symbol:${entity}`,
939
- `identity:${entity}`,
940
- `milestone:${entity}`,
941
- entity,
942
- ];
943
- for (const eid of candidates) {
944
- const sources = getBacklinks(index, eid);
945
- if (sources.length > 0) {
946
- const lines = sources.map(
947
- (s: any) => ` ${s.filePath}:${s.line} (${s.context})`,
948
- );
949
- return text(
950
- `Backlinks for ${eid} (${sources.length}):\n${lines.join('\n')}`,
951
- );
952
- }
953
- }
954
- return text(`No references found for: ${entity}`);
955
- }
956
-
957
- return text(`Unknown action: ${action}`);
958
- },
959
- );
960
-
961
- // -----------------------------------------------------------------------
962
- // Tool: trellis_refs_broken
963
- // -----------------------------------------------------------------------
964
- server.registerTool(
965
- 'trellis_refs_broken',
966
- {
967
- description:
968
- 'List all broken and stale wiki-link [[...]] references across the repository.',
969
- inputSchema: {
970
- path: z.string().default('.').describe('Repository root path'),
971
- },
972
- },
973
- async ({ path }) => {
974
- const engine = getEngine(path);
975
- const { readFileSync } = require('fs');
976
- const { join } = require('path');
977
- const {
978
- buildRefIndex,
979
- createResolverContext,
980
- resolveRef,
981
- StaleRefRegistry,
982
- getDiagnostics,
983
- } = require('../links/index.js');
984
-
985
- const resolverCtx = createResolverContext(engine);
986
- const rootPath = engine.getRootPath();
987
-
988
- // Build index
989
- const trackedFiles = engine.trackedFiles();
990
- const fileContents: Array<{ path: string; content: string }> = [];
991
- for (const f of trackedFiles) {
992
- try {
993
- const absPath = join(rootPath, f.path);
994
- const content = readFileSync(absPath, 'utf-8');
995
- fileContents.push({ path: f.path, content });
996
- } catch {}
997
- }
998
- const index = buildRefIndex(fileContents, resolverCtx);
999
-
1000
- // Resolve all refs
1001
- const registry = new StaleRefRegistry();
1002
- const resolvedIds = new Set<string>();
1003
- for (const [, refs] of index.outgoing) {
1004
- for (const ref of refs) {
1005
- const resolved = resolveRef(ref, resolverCtx);
1006
- if (resolved.state === 'resolved' && resolved.entityId) {
1007
- resolvedIds.add(resolved.entityId);
1008
- }
1009
- }
1010
- }
1011
-
1012
- const diags = getDiagnostics(index, registry, resolvedIds);
1013
- if (diags.length === 0)
1014
- return text('No broken or stale references found.');
1015
-
1016
- const broken = diags.filter((d: any) => d.state === 'broken');
1017
- const stale = diags.filter((d: any) => d.state === 'stale');
1018
-
1019
- const lines: string[] = [];
1020
- if (broken.length > 0) {
1021
- lines.push(`Broken references (${broken.length}):`);
1022
- for (const d of broken) {
1023
- lines.push(
1024
- ` ✗ ${d.source.filePath}:${d.source.line} — ${d.message}`,
1025
- );
1026
- }
1027
- }
1028
- if (stale.length > 0) {
1029
- if (lines.length > 0) lines.push('');
1030
- lines.push(`Stale references (${stale.length}):`);
1031
- for (const d of stale) {
1032
- lines.push(
1033
- ` ⚠ ${d.source.filePath}:${d.source.line} — ${d.message}`,
1034
- );
1035
- }
1036
- }
1037
-
1038
- return text(lines.join('\n'));
1039
- },
1040
- );
1041
-
1042
- // -----------------------------------------------------------------------
1043
- // Tool: trellis_search
1044
- // -----------------------------------------------------------------------
1045
- server.registerTool(
1046
- 'trellis_search',
1047
- {
1048
- description:
1049
- 'Semantic search across all embedded content (issues, milestones, markdown, code entities).',
1050
- inputSchema: {
1051
- path: z.string().default('.').describe('Repository root path'),
1052
- query: z.string().describe('Natural language search query'),
1053
- limit: z
1054
- .number()
1055
- .optional()
1056
- .default(10)
1057
- .describe('Max results (default: 10)'),
1058
- types: z
1059
- .string()
1060
- .optional()
1061
- .describe(
1062
- 'Comma-separated chunk types to filter (issue_title,issue_desc,milestone_msg,markdown,code_entity,doc_comment,summary_md)',
1063
- ),
1064
- },
1065
- },
1066
- async ({ path, query, limit, types }) => {
1067
- const engine = getEngine(path);
1068
- const { join } = require('path');
1069
- const { EmbeddingManager } = require('../embeddings/index.js');
1070
-
1071
- const rootPath = engine.getRootPath();
1072
- const dbPath = join(rootPath, '.trellis', 'embeddings.db');
1073
- const manager = new EmbeddingManager(dbPath);
1074
-
1075
- try {
1076
- const searchOpts: any = { limit: limit ?? 10 };
1077
- if (types) {
1078
- searchOpts.types = types.split(',').map((t: string) => t.trim());
1079
- }
1080
-
1081
- const results = await manager.search(query, searchOpts);
1082
-
1083
- if (results.length === 0) {
1084
- return text(
1085
- 'No results found. Run trellis_reindex to build the index.',
1086
- );
1087
- }
1088
-
1089
- const lines = results.map((r: any) => {
1090
- const score = (r.score * 100).toFixed(1);
1091
- const filePart = r.chunk.filePath ? ` (${r.chunk.filePath})` : '';
1092
- const preview = r.chunk.content.slice(0, 150).replace(/\n/g, ' ');
1093
- return `[${score}%] [${r.chunk.chunkType}]${filePart}\n ${preview}`;
1094
- });
1095
-
1096
- return text(
1097
- `Search results for "${query}" (${results.length}):\n\n${lines.join('\n\n')}`,
1098
- );
1099
- } finally {
1100
- manager.close();
1101
- }
1102
- },
1103
- );
1104
-
1105
- // -----------------------------------------------------------------------
1106
- // Tool: trellis_reindex
1107
- // -----------------------------------------------------------------------
1108
- server.registerTool(
1109
- 'trellis_reindex',
1110
- {
1111
- description:
1112
- 'Rebuild the semantic embedding index for all content in the repository.',
1113
- inputSchema: {
1114
- path: z.string().default('.').describe('Repository root path'),
1115
- },
1116
- },
1117
- async ({ path }) => {
1118
- const engine = getEngine(path);
1119
- const { join } = require('path');
1120
- const { EmbeddingManager } = require('../embeddings/index.js');
1121
-
1122
- const rootPath = engine.getRootPath();
1123
- const dbPath = join(rootPath, '.trellis', 'embeddings.db');
1124
- const manager = new EmbeddingManager(dbPath);
1125
-
1126
- try {
1127
- const result = await manager.reindex(engine);
1128
- const stats = manager.stats();
1129
-
1130
- return text(
1131
- [
1132
- `✓ Reindex complete: ${result.chunks} chunks embedded`,
1133
- `Types: ${JSON.stringify(stats.byType)}`,
1134
- ].join('\n'),
1135
- );
1136
- } finally {
1137
- manager.close();
1138
- }
1139
- },
1140
- );
1141
-
1142
- // -----------------------------------------------------------------------
1143
- // Tool: trellis_decision_list
1144
- // -----------------------------------------------------------------------
1145
- server.registerTool(
1146
- 'trellis_decision_list',
1147
- {
1148
- description:
1149
- 'List decision traces, optionally filtered by tool name pattern, agent, time range, or related entity.',
1150
- inputSchema: {
1151
- path: z.string().default('.').describe('Repository path'),
1152
- toolPattern: z
1153
- .string()
1154
- .optional()
1155
- .describe('Glob pattern for tool name (e.g. "trellis_issue_*")'),
1156
- entityId: z
1157
- .string()
1158
- .optional()
1159
- .describe('Only decisions referencing this entity ID'),
1160
- limit: z
1161
- .number()
1162
- .optional()
1163
- .default(20)
1164
- .describe('Max results (default 20)'),
1165
- },
1166
- },
1167
- async ({ path, toolPattern, entityId, limit }) => {
1168
- const engine = getEngine(path);
1169
- const decisions = engine.queryDecisions({
1170
- toolPattern: toolPattern ?? undefined,
1171
- entityId: entityId ?? undefined,
1172
- limit: limit ?? 20,
1173
- });
1174
- if (decisions.length === 0) {
1175
- return text('No decision traces found.');
1176
- }
1177
- const lines = decisions.map(
1178
- (d) =>
1179
- `${d.id} ${d.toolName} ${d.createdAt ?? ''}${d.rationale ? `\n → ${d.rationale}` : ''}`,
1180
- );
1181
- return text(lines.join('\n'));
1182
- },
1183
- );
1184
-
1185
- // -----------------------------------------------------------------------
1186
- // Tool: trellis_decision_show
1187
- // -----------------------------------------------------------------------
1188
- server.registerTool(
1189
- 'trellis_decision_show',
1190
- {
1191
- description: 'Show full details of a decision trace by ID.',
1192
- inputSchema: {
1193
- path: z.string().default('.').describe('Repository path'),
1194
- id: z.string().describe('Decision ID (e.g. DEC-1)'),
1195
- },
1196
- },
1197
- async ({ path, id }) => {
1198
- const engine = getEngine(path);
1199
- const decision = engine.getDecision(id);
1200
- if (!decision) {
1201
- return text(`Decision ${id} not found.`);
1202
- }
1203
- const lines = [
1204
- `ID: ${decision.id}`,
1205
- `Tool: ${decision.toolName}`,
1206
- `Created: ${decision.createdAt ?? 'unknown'}`,
1207
- `Agent: ${decision.createdBy ?? 'unknown'}`,
1208
- ];
1209
- if (decision.context) lines.push(`Context: ${decision.context}`);
1210
- if (decision.rationale) lines.push(`Rationale: ${decision.rationale}`);
1211
- if (decision.alternatives && decision.alternatives.length > 0) {
1212
- lines.push(`Alternatives: ${decision.alternatives.join(', ')}`);
1213
- }
1214
- if (decision.outputSummary) {
1215
- lines.push(`Output: ${decision.outputSummary}`);
1216
- }
1217
- if (decision.relatedEntities.length > 0) {
1218
- lines.push(`Related: ${decision.relatedEntities.join(', ')}`);
1219
- }
1220
- return text(lines.join('\n'));
1221
- },
1222
- );
1223
-
1224
- // -----------------------------------------------------------------------
1225
- // Tool: trellis_decision_chain
1226
- // -----------------------------------------------------------------------
1227
- server.registerTool(
1228
- 'trellis_decision_chain',
1229
- {
1230
- description:
1231
- 'Trace all decisions that affected a given entity (issue, file, milestone).',
1232
- inputSchema: {
1233
- path: z.string().default('.').describe('Repository path'),
1234
- entityId: z
1235
- .string()
1236
- .describe(
1237
- 'Entity ID to trace (e.g. "issue:TRL-5", "file:src/engine.ts")',
1238
- ),
1239
- },
1240
- },
1241
- async ({ path, entityId }) => {
1242
- const engine = getEngine(path);
1243
- const chain = engine.getDecisionChain(entityId);
1244
- if (chain.length === 0) {
1245
- return text(`No decision traces found for ${entityId}.`);
1246
- }
1247
- const lines = chain.map(
1248
- (d) =>
1249
- `${d.id} ${d.toolName} ${d.createdAt ?? ''}${d.rationale ? `\n → ${d.rationale}` : ''}`,
1250
- );
1251
- return text(
1252
- `Decision chain for ${entityId} (${chain.length} decisions):\n${lines.join('\n')}`,
1253
- );
1254
- },
1255
- );
1256
-
1257
- return server;
1258
- }
1259
-
1260
- // ---------------------------------------------------------------------------
1261
- // Auto-Capture Helpers (for external wiring)
1262
- // ---------------------------------------------------------------------------
1263
-
1264
- /**
1265
- * The shared hook registry for this MCP server instance.
1266
- * External agent harnesses can register pre/post hooks here.
1267
- */
1268
- export const hookRegistry = new HookRegistry();
1269
-
1270
- /**
1271
- * Create a DecisionRecorder that persists to a given repo path.
1272
- */
1273
- export function createRecorder(repoPath: string): DecisionRecorder {
1274
- return async (decision) => {
1275
- const engine = getEngine(repoPath);
1276
- await engine.recordDecision(decision);
1277
- };
1278
- }