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/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
|
-
}
|