@massu/core 0.1.0 → 0.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/LICENSE +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
6
|
+
import { getConfig } from './config.ts';
|
|
7
|
+
|
|
8
|
+
// ============================================================
|
|
9
|
+
// Team Knowledge Graph
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
/** Prefix a base tool name with the configured tool prefix. */
|
|
13
|
+
function p(baseName: string): string {
|
|
14
|
+
return `${getConfig().toolPrefix}_${baseName}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate expertise score for a developer in a module.
|
|
19
|
+
* Based on session depth (how many sessions) and observation quality.
|
|
20
|
+
*/
|
|
21
|
+
export function calculateExpertise(
|
|
22
|
+
sessionCount: number,
|
|
23
|
+
observationCount: number
|
|
24
|
+
): number {
|
|
25
|
+
const config = getConfig();
|
|
26
|
+
const sessionWeight = config.team?.expertise_weights?.session ?? 20;
|
|
27
|
+
const observationWeight = config.team?.expertise_weights?.observation ?? 10;
|
|
28
|
+
|
|
29
|
+
const sessionScore = Math.log2(sessionCount + 1) * sessionWeight;
|
|
30
|
+
const obsScore = Math.log2(observationCount + 1) * observationWeight;
|
|
31
|
+
return Math.min(100, Math.round(sessionScore + obsScore));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Update developer expertise based on session observations.
|
|
36
|
+
*/
|
|
37
|
+
export function updateExpertise(
|
|
38
|
+
db: Database.Database,
|
|
39
|
+
developerId: string,
|
|
40
|
+
sessionId: string
|
|
41
|
+
): void {
|
|
42
|
+
const fileChanges = db.prepare(`
|
|
43
|
+
SELECT DISTINCT files_involved FROM observations
|
|
44
|
+
WHERE session_id = ? AND type IN ('file_change', 'feature', 'bugfix', 'refactor')
|
|
45
|
+
`).all(sessionId) as Array<{ files_involved: string }>;
|
|
46
|
+
|
|
47
|
+
const modules = new Set<string>();
|
|
48
|
+
for (const fc of fileChanges) {
|
|
49
|
+
try {
|
|
50
|
+
const files = JSON.parse(fc.files_involved) as string[];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const module = extractModule(file);
|
|
53
|
+
if (module) modules.add(module);
|
|
54
|
+
}
|
|
55
|
+
} catch { /* skip */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const module of modules) {
|
|
59
|
+
const existing = db.prepare(
|
|
60
|
+
'SELECT session_count, observation_count FROM developer_expertise WHERE developer_id = ? AND module = ?'
|
|
61
|
+
).get(developerId, module) as { session_count: number; observation_count: number } | undefined;
|
|
62
|
+
|
|
63
|
+
const sessionCount = (existing?.session_count ?? 0) + 1;
|
|
64
|
+
const obsCount = (existing?.observation_count ?? 0) + fileChanges.length;
|
|
65
|
+
const score = calculateExpertise(sessionCount, obsCount);
|
|
66
|
+
|
|
67
|
+
db.prepare(`
|
|
68
|
+
INSERT INTO developer_expertise (developer_id, module, session_count, observation_count, expertise_score, last_active)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
|
70
|
+
ON CONFLICT(developer_id, module) DO UPDATE SET
|
|
71
|
+
session_count = ?,
|
|
72
|
+
observation_count = ?,
|
|
73
|
+
expertise_score = ?,
|
|
74
|
+
last_active = datetime('now')
|
|
75
|
+
`).run(
|
|
76
|
+
developerId, module, sessionCount, obsCount, score,
|
|
77
|
+
sessionCount, obsCount, score
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect potential conflicts between developers working on same files.
|
|
84
|
+
*/
|
|
85
|
+
export function detectConflicts(
|
|
86
|
+
db: Database.Database,
|
|
87
|
+
daysBack: number = 7
|
|
88
|
+
): Array<{
|
|
89
|
+
filePath: string;
|
|
90
|
+
developerA: string;
|
|
91
|
+
developerB: string;
|
|
92
|
+
conflictType: string;
|
|
93
|
+
}> {
|
|
94
|
+
const conflicts = db.prepare(`
|
|
95
|
+
SELECT so1.file_path,
|
|
96
|
+
so1.developer_id as developer_a,
|
|
97
|
+
so2.developer_id as developer_b,
|
|
98
|
+
'concurrent_edit' as conflict_type
|
|
99
|
+
FROM shared_observations so1
|
|
100
|
+
JOIN shared_observations so2 ON so1.file_path = so2.file_path
|
|
101
|
+
WHERE so1.developer_id != so2.developer_id
|
|
102
|
+
AND so1.file_path IS NOT NULL
|
|
103
|
+
AND so1.created_at >= datetime('now', ?)
|
|
104
|
+
AND so2.created_at >= datetime('now', ?)
|
|
105
|
+
GROUP BY so1.file_path, so1.developer_id, so2.developer_id
|
|
106
|
+
`).all(`-${daysBack} days`, `-${daysBack} days`) as Array<{
|
|
107
|
+
file_path: string;
|
|
108
|
+
developer_a: string;
|
|
109
|
+
developer_b: string;
|
|
110
|
+
conflict_type: string;
|
|
111
|
+
}>;
|
|
112
|
+
|
|
113
|
+
return conflicts.map(c => ({
|
|
114
|
+
filePath: c.file_path,
|
|
115
|
+
developerA: c.developer_a,
|
|
116
|
+
developerB: c.developer_b,
|
|
117
|
+
conflictType: c.conflict_type,
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Share an observation for team visibility.
|
|
123
|
+
*/
|
|
124
|
+
export function shareObservation(
|
|
125
|
+
db: Database.Database,
|
|
126
|
+
developerId: string,
|
|
127
|
+
project: string,
|
|
128
|
+
observationType: string,
|
|
129
|
+
summary: string,
|
|
130
|
+
opts?: {
|
|
131
|
+
originalId?: number;
|
|
132
|
+
filePath?: string;
|
|
133
|
+
module?: string;
|
|
134
|
+
severity?: number;
|
|
135
|
+
}
|
|
136
|
+
): number {
|
|
137
|
+
const result = db.prepare(`
|
|
138
|
+
INSERT INTO shared_observations
|
|
139
|
+
(original_id, developer_id, project, observation_type, summary, file_path, module, severity, is_shared, shared_at)
|
|
140
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, datetime('now'))
|
|
141
|
+
`).run(
|
|
142
|
+
opts?.originalId ?? null,
|
|
143
|
+
developerId, project, observationType, summary,
|
|
144
|
+
opts?.filePath ?? null,
|
|
145
|
+
opts?.module ?? null,
|
|
146
|
+
opts?.severity ?? 3
|
|
147
|
+
);
|
|
148
|
+
return Number(result.lastInsertRowid);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract business module from a file path.
|
|
153
|
+
* Uses configurable module extraction patterns if provided.
|
|
154
|
+
*/
|
|
155
|
+
function extractModule(filePath: string): string | null {
|
|
156
|
+
// Route-based modules
|
|
157
|
+
const routerMatch = filePath.match(/routers\/([^/.]+)/);
|
|
158
|
+
if (routerMatch) return routerMatch[1];
|
|
159
|
+
|
|
160
|
+
// Page-based modules
|
|
161
|
+
const pageMatch = filePath.match(/app\/\(([^)]+)\)/);
|
|
162
|
+
if (pageMatch) return pageMatch[1];
|
|
163
|
+
|
|
164
|
+
// Component-based
|
|
165
|
+
const compMatch = filePath.match(/components\/([^/.]+)/);
|
|
166
|
+
if (compMatch) return compMatch[1];
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// MCP Tool Definitions & Handlers
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
export function getTeamToolDefinitions(): ToolDefinition[] {
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
name: p('team_search'),
|
|
179
|
+
description: 'Search team-shared observations. Find what other developers learned about a module or file.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
query: { type: 'string', description: 'Search text' },
|
|
184
|
+
module: { type: 'string', description: 'Filter by business module' },
|
|
185
|
+
},
|
|
186
|
+
required: ['query'],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: p('team_expertise'),
|
|
191
|
+
description: 'Who knows what. Shows developers ranked by expertise for a module or file area.',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
module: { type: 'string', description: 'Business module (e.g., orders, products, design)' },
|
|
196
|
+
file_path: { type: 'string', description: 'File path to find experts for' },
|
|
197
|
+
},
|
|
198
|
+
required: [],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: p('team_conflicts'),
|
|
203
|
+
description: 'Detect concurrent work conflicts. Find areas where multiple developers are making changes.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
file_path: { type: 'string', description: 'Check specific file for conflicts' },
|
|
208
|
+
days: { type: 'number', description: 'Days to look back (default: 7)' },
|
|
209
|
+
},
|
|
210
|
+
required: [],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const TEAM_BASE_NAMES = new Set(['team_search', 'team_expertise', 'team_conflicts']);
|
|
217
|
+
|
|
218
|
+
export function isTeamTool(name: string): boolean {
|
|
219
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
220
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
221
|
+
return TEAM_BASE_NAMES.has(baseName);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function handleTeamToolCall(
|
|
225
|
+
name: string,
|
|
226
|
+
args: Record<string, unknown>,
|
|
227
|
+
memoryDb: Database.Database
|
|
228
|
+
): ToolResult {
|
|
229
|
+
try {
|
|
230
|
+
const pfx = getConfig().toolPrefix + '_';
|
|
231
|
+
const baseName = name.startsWith(pfx) ? name.slice(pfx.length) : name;
|
|
232
|
+
|
|
233
|
+
switch (baseName) {
|
|
234
|
+
case 'team_search':
|
|
235
|
+
return handleTeamSearch(args, memoryDb);
|
|
236
|
+
case 'team_expertise':
|
|
237
|
+
return handleTeamExpertise(args, memoryDb);
|
|
238
|
+
case 'team_conflicts':
|
|
239
|
+
return handleTeamConflicts(args, memoryDb);
|
|
240
|
+
default:
|
|
241
|
+
return text(`Unknown team tool: ${name}`);
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
return text(`Error in ${name}: ${error instanceof Error ? error.message : String(error)}\n\nUsage: ${p('team_search')} { query: "pattern" }, ${p('team_expertise')} { module: "tasks" }`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function handleTeamSearch(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
249
|
+
const query = args.query as string;
|
|
250
|
+
if (!query) return text(`Usage: ${p('team_search')} { query: "search term", module: "optional-module" } - Search team-shared observations.`);
|
|
251
|
+
|
|
252
|
+
const module = args.module as string | undefined;
|
|
253
|
+
|
|
254
|
+
let sql = `
|
|
255
|
+
SELECT id, developer_id, observation_type, summary, file_path, module, severity, created_at
|
|
256
|
+
FROM shared_observations
|
|
257
|
+
WHERE is_shared = TRUE AND summary LIKE ?
|
|
258
|
+
`;
|
|
259
|
+
const params: (string | number)[] = [`%${query}%`];
|
|
260
|
+
|
|
261
|
+
if (module) {
|
|
262
|
+
sql += ' AND module = ?';
|
|
263
|
+
params.push(module);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
sql += ' ORDER BY created_at DESC LIMIT 20';
|
|
267
|
+
|
|
268
|
+
const results = db.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
269
|
+
|
|
270
|
+
if (results.length === 0) {
|
|
271
|
+
return text(`No shared observations found for "${query}". Team knowledge is populated when developers share observations across sessions. Try: ${p('team_expertise')} {} to see module expertise, or broaden your search term.`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lines = [
|
|
275
|
+
`## Team Knowledge: "${query}" (${results.length} results)`,
|
|
276
|
+
'',
|
|
277
|
+
'| Developer | Type | Summary | Module | Date |',
|
|
278
|
+
'|-----------|------|---------|--------|------|',
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
for (const r of results) {
|
|
282
|
+
lines.push(
|
|
283
|
+
`| ${r.developer_id} | ${r.observation_type} | ${(r.summary as string).slice(0, 60)} | ${r.module ?? '-'} | ${(r.created_at as string).split('T')[0]} |`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return text(lines.join('\n'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleTeamExpertise(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
291
|
+
const module = args.module as string | undefined;
|
|
292
|
+
const filePath = args.file_path as string | undefined;
|
|
293
|
+
|
|
294
|
+
let targetModule = module;
|
|
295
|
+
if (!targetModule && filePath) {
|
|
296
|
+
targetModule = extractModule(filePath) ?? undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!targetModule) {
|
|
300
|
+
const modules = db.prepare(`
|
|
301
|
+
SELECT module, COUNT(DISTINCT developer_id) as developers, MAX(expertise_score) as top_score
|
|
302
|
+
FROM developer_expertise
|
|
303
|
+
GROUP BY module
|
|
304
|
+
ORDER BY developers DESC
|
|
305
|
+
`).all() as Array<Record<string, unknown>>;
|
|
306
|
+
|
|
307
|
+
if (modules.length === 0) {
|
|
308
|
+
return text(`No expertise data yet. Expertise is built automatically as developers work on modules across sessions. Try: ${p('team_search')} { query: "keyword" } to search shared observations instead.`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const lines = [
|
|
312
|
+
'## Team Expertise Overview',
|
|
313
|
+
'',
|
|
314
|
+
'| Module | Developers | Top Score |',
|
|
315
|
+
'|--------|-----------|-----------|',
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
for (const m of modules) {
|
|
319
|
+
lines.push(`| ${m.module} | ${m.developers} | ${m.top_score} |`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push(`Use ${p('team_expertise')} { module: "module_name" } to see developers ranked by expertise.`);
|
|
324
|
+
|
|
325
|
+
return text(lines.join('\n'));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const experts = db.prepare(`
|
|
329
|
+
SELECT developer_id, expertise_score, session_count, observation_count, last_active
|
|
330
|
+
FROM developer_expertise
|
|
331
|
+
WHERE module = ?
|
|
332
|
+
ORDER BY expertise_score DESC
|
|
333
|
+
`).all(targetModule) as Array<Record<string, unknown>>;
|
|
334
|
+
|
|
335
|
+
if (experts.length === 0) {
|
|
336
|
+
return text(`No expertise data for module "${targetModule}". Expertise builds as developers work on files in this module across sessions. Try: ${p('team_expertise')} {} to see all modules with tracked expertise.`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const lines = [
|
|
340
|
+
`## Expertise: ${targetModule}`,
|
|
341
|
+
'',
|
|
342
|
+
'| Developer | Score | Sessions | Observations | Last Active |',
|
|
343
|
+
'|-----------|-------|----------|--------------|-------------|',
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
for (const e of experts) {
|
|
347
|
+
lines.push(
|
|
348
|
+
`| ${e.developer_id} | ${e.expertise_score} | ${e.session_count} | ${e.observation_count} | ${(e.last_active as string).split('T')[0]} |`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return text(lines.join('\n'));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function handleTeamConflicts(args: Record<string, unknown>, db: Database.Database): ToolResult {
|
|
356
|
+
const days = (args.days as number) ?? 7;
|
|
357
|
+
const filePath = args.file_path as string | undefined;
|
|
358
|
+
|
|
359
|
+
if (filePath) {
|
|
360
|
+
const conflicts = db.prepare(`
|
|
361
|
+
SELECT developer_a, developer_b, conflict_type, detected_at, resolved
|
|
362
|
+
FROM knowledge_conflicts
|
|
363
|
+
WHERE file_path = ?
|
|
364
|
+
ORDER BY detected_at DESC LIMIT 10
|
|
365
|
+
`).all(filePath) as Array<Record<string, unknown>>;
|
|
366
|
+
|
|
367
|
+
if (conflicts.length === 0) {
|
|
368
|
+
return text(`No conflicts detected for "${filePath}". Conflicts are detected when multiple developers modify the same file within the lookback window. Try: ${p('team_conflicts')} { days: 30 } to check for conflicts across all files.`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const lines = [
|
|
372
|
+
`## Conflicts: ${filePath}`,
|
|
373
|
+
'',
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
for (const c of conflicts) {
|
|
377
|
+
lines.push(`- ${c.developer_a} vs ${c.developer_b} (${c.conflict_type}) - ${c.resolved ? 'resolved' : 'ACTIVE'}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return text(lines.join('\n'));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// General conflict detection
|
|
384
|
+
const conflicts = detectConflicts(db, days);
|
|
385
|
+
|
|
386
|
+
if (conflicts.length === 0) {
|
|
387
|
+
return text(`No concurrent work conflicts detected in the last ${days} days. Conflicts are tracked when multiple developers modify the same files. Try a longer time range: ${p('team_conflicts')} { days: 90 }.`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const lines = [
|
|
391
|
+
`## Work Conflicts (${days} days)`,
|
|
392
|
+
`Detected: ${conflicts.length}`,
|
|
393
|
+
'',
|
|
394
|
+
'| File | Developer A | Developer B | Type |',
|
|
395
|
+
'|------|-----------|-----------|------|',
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
for (const c of conflicts) {
|
|
399
|
+
lines.push(`| ${c.filePath} | ${c.developerA} | ${c.developerB} | ${c.conflictType} |`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return text(lines.join('\n'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function text(content: string): ToolResult {
|
|
406
|
+
return { content: [{ type: 'text', text: content }] };
|
|
407
|
+
}
|