@juspay/yama 1.6.0 → 2.1.0
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/.mcp-config.example.json +26 -0
- package/CHANGELOG.md +46 -0
- package/README.md +311 -685
- package/dist/cli/v2.cli.d.ts +13 -0
- package/dist/cli/v2.cli.js +359 -0
- package/dist/index.d.ts +12 -13
- package/dist/index.js +18 -19
- package/dist/v2/config/ConfigLoader.d.ts +50 -0
- package/dist/v2/config/ConfigLoader.js +205 -0
- package/dist/v2/config/DefaultConfig.d.ts +9 -0
- package/dist/v2/config/DefaultConfig.js +187 -0
- package/dist/v2/core/LearningOrchestrator.d.ts +65 -0
- package/dist/v2/core/LearningOrchestrator.js +499 -0
- package/dist/v2/core/MCPServerManager.d.ts +22 -0
- package/dist/v2/core/MCPServerManager.js +100 -0
- package/dist/v2/core/SessionManager.d.ts +72 -0
- package/dist/v2/core/SessionManager.js +200 -0
- package/dist/v2/core/YamaV2Orchestrator.d.ts +112 -0
- package/dist/v2/core/YamaV2Orchestrator.js +549 -0
- package/dist/v2/learning/FeedbackExtractor.d.ts +46 -0
- package/dist/v2/learning/FeedbackExtractor.js +237 -0
- package/dist/v2/learning/KnowledgeBaseManager.d.ts +91 -0
- package/dist/v2/learning/KnowledgeBaseManager.js +475 -0
- package/dist/v2/learning/types.d.ts +121 -0
- package/dist/v2/learning/types.js +15 -0
- package/dist/v2/prompts/EnhancementSystemPrompt.d.ts +8 -0
- package/dist/v2/prompts/EnhancementSystemPrompt.js +216 -0
- package/dist/v2/prompts/LangfusePromptManager.d.ts +48 -0
- package/dist/v2/prompts/LangfusePromptManager.js +144 -0
- package/dist/v2/prompts/LearningSystemPrompt.d.ts +11 -0
- package/dist/v2/prompts/LearningSystemPrompt.js +180 -0
- package/dist/v2/prompts/PromptBuilder.d.ts +45 -0
- package/dist/v2/prompts/PromptBuilder.js +257 -0
- package/dist/v2/prompts/ReviewSystemPrompt.d.ts +8 -0
- package/dist/v2/prompts/ReviewSystemPrompt.js +270 -0
- package/dist/v2/types/config.types.d.ts +141 -0
- package/dist/v2/types/config.types.js +5 -0
- package/dist/v2/types/mcp.types.d.ts +191 -0
- package/dist/v2/types/mcp.types.js +6 -0
- package/dist/v2/types/v2.types.d.ts +182 -0
- package/dist/v2/types/v2.types.js +42 -0
- package/dist/v2/utils/ObservabilityConfig.d.ts +22 -0
- package/dist/v2/utils/ObservabilityConfig.js +48 -0
- package/package.json +16 -10
- package/yama.config.example.yaml +259 -204
- package/dist/cli/index.d.ts +0 -12
- package/dist/cli/index.js +0 -538
- package/dist/core/ContextGatherer.d.ts +0 -110
- package/dist/core/ContextGatherer.js +0 -470
- package/dist/core/Guardian.d.ts +0 -81
- package/dist/core/Guardian.js +0 -480
- package/dist/core/providers/BitbucketProvider.d.ts +0 -105
- package/dist/core/providers/BitbucketProvider.js +0 -489
- package/dist/features/CodeReviewer.d.ts +0 -173
- package/dist/features/CodeReviewer.js +0 -1707
- package/dist/features/DescriptionEnhancer.d.ts +0 -70
- package/dist/features/DescriptionEnhancer.js +0 -511
- package/dist/features/MultiInstanceProcessor.d.ts +0 -74
- package/dist/features/MultiInstanceProcessor.js +0 -360
- package/dist/types/index.d.ts +0 -624
- package/dist/types/index.js +0 -104
- package/dist/utils/Cache.d.ts +0 -103
- package/dist/utils/Cache.js +0 -444
- package/dist/utils/ConfigManager.d.ts +0 -88
- package/dist/utils/ConfigManager.js +0 -602
- package/dist/utils/ContentSimilarityService.d.ts +0 -74
- package/dist/utils/ContentSimilarityService.js +0 -215
- package/dist/utils/ExactDuplicateRemover.d.ts +0 -77
- package/dist/utils/ExactDuplicateRemover.js +0 -361
- package/dist/utils/Logger.d.ts +0 -31
- package/dist/utils/Logger.js +0 -214
- package/dist/utils/MemoryBankManager.d.ts +0 -73
- package/dist/utils/MemoryBankManager.js +0 -310
- package/dist/utils/ParallelProcessing.d.ts +0 -140
- package/dist/utils/ParallelProcessing.js +0 -333
- package/dist/utils/ProviderLimits.d.ts +0 -58
- package/dist/utils/ProviderLimits.js +0 -143
- package/dist/utils/RetryManager.d.ts +0 -78
- package/dist/utils/RetryManager.js +0 -205
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning Orchestrator
|
|
3
|
+
* Main entry point for the knowledge base learning feature
|
|
4
|
+
* Orchestrates PR comment extraction, AI analysis, and knowledge base updates
|
|
5
|
+
*/
|
|
6
|
+
import { NeuroLink } from "@juspay/neurolink";
|
|
7
|
+
import { MCPServerManager } from "./MCPServerManager.js";
|
|
8
|
+
import { ConfigLoader } from "../config/ConfigLoader.js";
|
|
9
|
+
import { LangfusePromptManager } from "../prompts/LangfusePromptManager.js";
|
|
10
|
+
import { KnowledgeBaseManager } from "../learning/KnowledgeBaseManager.js";
|
|
11
|
+
import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
|
|
12
|
+
export class LearningOrchestrator {
|
|
13
|
+
neurolink;
|
|
14
|
+
mcpManager;
|
|
15
|
+
configLoader;
|
|
16
|
+
promptManager;
|
|
17
|
+
config;
|
|
18
|
+
initialized = false;
|
|
19
|
+
constructor() {
|
|
20
|
+
this.configLoader = new ConfigLoader();
|
|
21
|
+
this.mcpManager = new MCPServerManager();
|
|
22
|
+
this.promptManager = new LangfusePromptManager();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the learning orchestrator
|
|
26
|
+
*/
|
|
27
|
+
async initialize(configPath) {
|
|
28
|
+
if (this.initialized) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log("🧠 Initializing Learning Orchestrator...\n");
|
|
32
|
+
try {
|
|
33
|
+
// Load configuration
|
|
34
|
+
this.config = await this.configLoader.loadConfig(configPath);
|
|
35
|
+
// Initialize NeuroLink
|
|
36
|
+
console.log(" 🔧 Initializing NeuroLink AI engine...");
|
|
37
|
+
this.neurolink = this.initializeNeurolink();
|
|
38
|
+
console.log(" ✅ NeuroLink initialized\n");
|
|
39
|
+
// Setup MCP servers (need Bitbucket for PR access)
|
|
40
|
+
await this.mcpManager.setupMCPServers(this.neurolink, this.config.mcpServers);
|
|
41
|
+
console.log(" ✅ MCP servers ready\n");
|
|
42
|
+
this.initialized = true;
|
|
43
|
+
console.log("✅ Learning Orchestrator initialized\n");
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("\n❌ Initialization failed:", error.message);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract learnings from a merged PR
|
|
52
|
+
* Uses fully agentic AI approach - AI decides which tools to call
|
|
53
|
+
*/
|
|
54
|
+
async extractLearnings(request) {
|
|
55
|
+
await this.ensureInitialized();
|
|
56
|
+
console.log("\n" + "─".repeat(60));
|
|
57
|
+
console.log(`📚 Extracting learnings from PR #${request.pullRequestId}`);
|
|
58
|
+
console.log("─".repeat(60));
|
|
59
|
+
console.log(` Workspace: ${request.workspace}`);
|
|
60
|
+
console.log(` Repository: ${request.repository}`);
|
|
61
|
+
console.log(` Mode: ${request.dryRun ? "🔵 DRY RUN" : "🔴 LIVE"}`);
|
|
62
|
+
console.log("─".repeat(60) + "\n");
|
|
63
|
+
try {
|
|
64
|
+
// STEP 1: Fetch PR comments using agentic approach (with tools)
|
|
65
|
+
console.log("📥 Step 1: Fetching PR comments via Bitbucket MCP...");
|
|
66
|
+
const fetchInstructions = this.buildFetchCommentsInstructions(request);
|
|
67
|
+
const fetchResponse = await this.neurolink.generate({
|
|
68
|
+
input: { text: fetchInstructions },
|
|
69
|
+
provider: this.config.ai.provider,
|
|
70
|
+
model: this.config.ai.model,
|
|
71
|
+
temperature: 0.1,
|
|
72
|
+
maxTokens: 50000,
|
|
73
|
+
timeout: this.config.ai.timeout,
|
|
74
|
+
context: {
|
|
75
|
+
operation: "learning-fetch-comments",
|
|
76
|
+
prId: request.pullRequestId,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const commentsData = fetchResponse.content || "";
|
|
80
|
+
console.log(" ✅ PR data fetched\n");
|
|
81
|
+
// STEP 2: Extract learnings with structured schema (no tools needed)
|
|
82
|
+
console.log("🤖 Step 2: Extracting learnings with structured output...");
|
|
83
|
+
const learningPrompt = await this.promptManager.getLearningPrompt();
|
|
84
|
+
const extractionInstructions = this.buildExtractionInstructions(request, learningPrompt, commentsData);
|
|
85
|
+
// Extract learnings - disableTools since we already have the data
|
|
86
|
+
const extractResponse = await this.neurolink.generate({
|
|
87
|
+
input: { text: extractionInstructions },
|
|
88
|
+
provider: this.config.ai.provider,
|
|
89
|
+
model: this.config.ai.model,
|
|
90
|
+
temperature: 0.2,
|
|
91
|
+
maxTokens: 10000,
|
|
92
|
+
timeout: "2m",
|
|
93
|
+
disableTools: true, // No tools needed for extraction phase
|
|
94
|
+
context: {
|
|
95
|
+
operation: "learning-extraction",
|
|
96
|
+
prId: request.pullRequestId,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
// Parse structured response
|
|
100
|
+
const learnings = this.parseStructuredLearnings(extractResponse, request.pullRequestId);
|
|
101
|
+
console.log(`\n📊 AI extracted ${learnings.length} learnings\n`);
|
|
102
|
+
if (learnings.length === 0) {
|
|
103
|
+
// Log extraction response for debugging
|
|
104
|
+
console.log("🔍 Extraction Response (for debugging):");
|
|
105
|
+
console.log("─".repeat(60));
|
|
106
|
+
const responsePreview = JSON.stringify(extractResponse.content || extractResponse, null, 2).slice(0, 2000);
|
|
107
|
+
console.log(responsePreview);
|
|
108
|
+
console.log("─".repeat(60) + "\n");
|
|
109
|
+
return this.createEmptyResult(request, "No actionable learnings found from PR feedback");
|
|
110
|
+
}
|
|
111
|
+
// Handle dry run vs live mode
|
|
112
|
+
if (request.dryRun) {
|
|
113
|
+
return this.handleDryRun(request, learnings);
|
|
114
|
+
}
|
|
115
|
+
// Update knowledge base
|
|
116
|
+
console.log("📝 Updating knowledge base...");
|
|
117
|
+
const kbManager = new KnowledgeBaseManager(this.config.knowledgeBase, process.cwd());
|
|
118
|
+
// Create if doesn't exist
|
|
119
|
+
if (!kbManager.exists()) {
|
|
120
|
+
console.log(" Creating new knowledge base file...");
|
|
121
|
+
await kbManager.create();
|
|
122
|
+
}
|
|
123
|
+
const addedCount = await kbManager.append(learnings);
|
|
124
|
+
const duplicateCount = learnings.length - addedCount;
|
|
125
|
+
console.log(` ✅ Added ${addedCount} learnings`);
|
|
126
|
+
if (duplicateCount > 0) {
|
|
127
|
+
console.log(` ⏭️ Skipped ${duplicateCount} duplicates`);
|
|
128
|
+
}
|
|
129
|
+
// Check if summarization is needed
|
|
130
|
+
let summarized = false;
|
|
131
|
+
if (request.summarize || (await kbManager.needsSummarization())) {
|
|
132
|
+
console.log("\n🔄 Running summarization...");
|
|
133
|
+
await this.runSummarization(kbManager);
|
|
134
|
+
summarized = true;
|
|
135
|
+
console.log(" ✅ Summarization complete");
|
|
136
|
+
}
|
|
137
|
+
// Commit if requested
|
|
138
|
+
let committed = false;
|
|
139
|
+
if (request.commit || this.config.knowledgeBase.autoCommit) {
|
|
140
|
+
console.log("\n📤 Committing knowledge base...");
|
|
141
|
+
await kbManager.commit(request.pullRequestId, addedCount);
|
|
142
|
+
committed = true;
|
|
143
|
+
console.log(" ✅ Changes committed");
|
|
144
|
+
}
|
|
145
|
+
// Return result
|
|
146
|
+
const result = {
|
|
147
|
+
success: true,
|
|
148
|
+
prId: request.pullRequestId,
|
|
149
|
+
learningsFound: learnings.length,
|
|
150
|
+
learningsAdded: addedCount,
|
|
151
|
+
learningsDuplicate: duplicateCount,
|
|
152
|
+
learnings,
|
|
153
|
+
knowledgeBasePath: kbManager.getFilePath(),
|
|
154
|
+
committed,
|
|
155
|
+
summarized,
|
|
156
|
+
};
|
|
157
|
+
this.logResult(result);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error("\n❌ Learning extraction failed:", error.message);
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
prId: request.pullRequestId,
|
|
165
|
+
learningsFound: 0,
|
|
166
|
+
learningsAdded: 0,
|
|
167
|
+
learningsDuplicate: 0,
|
|
168
|
+
learnings: [],
|
|
169
|
+
error: error.message,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Build instructions for Step 1: Fetch PR comments
|
|
175
|
+
*/
|
|
176
|
+
buildFetchCommentsInstructions(request) {
|
|
177
|
+
const aiPatterns = this.config.knowledgeBase.aiAuthorPatterns.join(", ");
|
|
178
|
+
return `
|
|
179
|
+
<task>Fetch and analyze ALL PR comments for learning extraction</task>
|
|
180
|
+
|
|
181
|
+
<pr-details>
|
|
182
|
+
<workspace>${request.workspace}</workspace>
|
|
183
|
+
<repository>${request.repository}</repository>
|
|
184
|
+
<pull_request_id>${request.pullRequestId}</pull_request_id>
|
|
185
|
+
</pr-details>
|
|
186
|
+
|
|
187
|
+
<instructions>
|
|
188
|
+
1. Use get_pull_request tool to fetch the PR details including all comments/activities
|
|
189
|
+
2. Look through ALL comments on the PR (both active and resolved if available)
|
|
190
|
+
3. Identify AI-generated comments by:
|
|
191
|
+
- Author patterns: ${aiPatterns}
|
|
192
|
+
- Text patterns: Comments starting with "🔒 CRITICAL:", "⚠️ MAJOR:", "💡 MINOR:", "💬 SUGGESTION:", or "Yama Review"
|
|
193
|
+
4. For each AI comment, look for developer replies (threaded responses or comments at same location)
|
|
194
|
+
5. IMPORTANT: Also identify DEVELOPER comments that are NOT replies to AI comments
|
|
195
|
+
- These represent issues that AI MISSED and should have caught
|
|
196
|
+
- Look for inline code comments from developers (not AI) that point out bugs, issues, or improvements
|
|
197
|
+
6. Output a structured summary of what you found
|
|
198
|
+
</instructions>
|
|
199
|
+
|
|
200
|
+
<output-format>
|
|
201
|
+
Provide a detailed summary in this format:
|
|
202
|
+
|
|
203
|
+
## PR Overview
|
|
204
|
+
- Title: [PR title]
|
|
205
|
+
- State: [merged/open/etc]
|
|
206
|
+
- Total comments found: [N]
|
|
207
|
+
|
|
208
|
+
## AI Comments Found
|
|
209
|
+
For each AI comment, list:
|
|
210
|
+
- Author: [name]
|
|
211
|
+
- Severity: [CRITICAL/MAJOR/MINOR/SUGGESTION]
|
|
212
|
+
- Comment text: [the comment]
|
|
213
|
+
- Location: [file path and line if inline]
|
|
214
|
+
|
|
215
|
+
## Developer Replies to AI Comments
|
|
216
|
+
For each developer reply to an AI comment:
|
|
217
|
+
- Original AI comment: [brief summary]
|
|
218
|
+
- Developer reply: [the full reply text]
|
|
219
|
+
- Developer name: [name]
|
|
220
|
+
|
|
221
|
+
## Developer Comments (NOT replies to AI)
|
|
222
|
+
IMPORTANT: List ALL developer-authored inline comments that are NOT replies to AI.
|
|
223
|
+
These represent issues AI MISSED and should have caught.
|
|
224
|
+
For each:
|
|
225
|
+
- Developer name: [name]
|
|
226
|
+
- File: [file path]
|
|
227
|
+
- Line: [line number if available]
|
|
228
|
+
- Comment text: [the full comment]
|
|
229
|
+
- Type: [bug/issue/suggestion/question]
|
|
230
|
+
|
|
231
|
+
If there are no developer comments (other than replies), state: "NO INDEPENDENT DEVELOPER COMMENTS FOUND"
|
|
232
|
+
</output-format>
|
|
233
|
+
|
|
234
|
+
Fetch the PR data now.
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build instructions for Step 2: Extract learnings from fetched data
|
|
239
|
+
*/
|
|
240
|
+
buildExtractionInstructions(request, learningPrompt, commentsData) {
|
|
241
|
+
return `
|
|
242
|
+
<task>Extract project-level learnings from PR feedback</task>
|
|
243
|
+
|
|
244
|
+
<fetched-pr-data>
|
|
245
|
+
${commentsData}
|
|
246
|
+
</fetched-pr-data>
|
|
247
|
+
|
|
248
|
+
${learningPrompt}
|
|
249
|
+
|
|
250
|
+
<extraction-rules>
|
|
251
|
+
Extract learnings from TWO sources:
|
|
252
|
+
|
|
253
|
+
1. DEVELOPER REPLIES TO AI COMMENTS:
|
|
254
|
+
- Developer explained why AI was wrong → false_positive
|
|
255
|
+
- Developer provided context AI should know → domain_context
|
|
256
|
+
- Developer indicated team preference → style_preference
|
|
257
|
+
- Developer suggested how AI should improve → enhancement_guideline
|
|
258
|
+
|
|
259
|
+
2. INDEPENDENT DEVELOPER COMMENTS (not replies to AI):
|
|
260
|
+
- These are issues AI MISSED and should have caught → missed_issue
|
|
261
|
+
- If a developer added their own inline comment about a bug/issue, AI failed to spot it
|
|
262
|
+
- Extract what type of issue it was so AI can catch similar issues in future
|
|
263
|
+
|
|
264
|
+
IMPORTANT RULES:
|
|
265
|
+
- Make learnings GENERIC - remove PR-specific details (file names, line numbers, variable names)
|
|
266
|
+
- Focus on the underlying principle or pattern that can apply to future reviews
|
|
267
|
+
- If BOTH "NO DEVELOPER REPLIES" AND "NO INDEPENDENT DEVELOPER COMMENTS", return empty learnings
|
|
268
|
+
|
|
269
|
+
Categories:
|
|
270
|
+
- false_positive: Developer explained why AI was wrong
|
|
271
|
+
- missed_issue: Developer pointed out something AI should have caught (including independent comments)
|
|
272
|
+
- style_preference: Team conventions that differ from general practice
|
|
273
|
+
- domain_context: Project-specific knowledge AI needs
|
|
274
|
+
- enhancement_guideline: How AI should provide better suggestions
|
|
275
|
+
</extraction-rules>
|
|
276
|
+
|
|
277
|
+
<output-format>
|
|
278
|
+
Return a JSON object in this exact format (wrapped in a json code block):
|
|
279
|
+
|
|
280
|
+
\`\`\`json
|
|
281
|
+
{
|
|
282
|
+
"learnings": [
|
|
283
|
+
{
|
|
284
|
+
"category": "false_positive|missed_issue|style_preference|domain_context|enhancement_guideline",
|
|
285
|
+
"subcategory": "Optional grouping (e.g., 'Async Patterns', 'Error Handling')",
|
|
286
|
+
"learning": "The GENERIC, PROJECT-LEVEL guideline",
|
|
287
|
+
"filePatterns": ["Optional array of file patterns where this applies"],
|
|
288
|
+
"reasoning": "Why this learning was extracted"
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
"summary": {
|
|
292
|
+
"totalAIComments": 0,
|
|
293
|
+
"totalDeveloperReplies": 0,
|
|
294
|
+
"totalIndependentDevComments": 0,
|
|
295
|
+
"actionablePairsFound": 0
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
\`\`\`
|
|
299
|
+
|
|
300
|
+
If no actionable feedback found, return empty learnings array with appropriate summary counts.
|
|
301
|
+
</output-format>
|
|
302
|
+
|
|
303
|
+
Analyze the PR data above and extract learnings.
|
|
304
|
+
`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Parse structured response from AI output
|
|
308
|
+
*/
|
|
309
|
+
parseStructuredLearnings(response, prId) {
|
|
310
|
+
try {
|
|
311
|
+
const content = (response.content || "");
|
|
312
|
+
// Find JSON in response (may be in code block)
|
|
313
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) ||
|
|
314
|
+
content.match(/\{[\s\S]*\}/);
|
|
315
|
+
if (!jsonMatch) {
|
|
316
|
+
console.log(" ⚠️ No JSON found in extraction response");
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
const jsonStr = jsonMatch[1] || jsonMatch[0];
|
|
320
|
+
const parsed = JSON.parse(jsonStr);
|
|
321
|
+
// Log summary if available
|
|
322
|
+
if (parsed.summary) {
|
|
323
|
+
const devComments = parsed.summary.totalIndependentDevComments || 0;
|
|
324
|
+
console.log(` 📊 Summary: ${parsed.summary.totalAIComments} AI comments, ${parsed.summary.totalDeveloperReplies} dev replies, ${devComments} independent dev comments`);
|
|
325
|
+
}
|
|
326
|
+
if (!parsed.learnings || !Array.isArray(parsed.learnings)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const kbManager = new KnowledgeBaseManager(this.config.knowledgeBase, process.cwd());
|
|
330
|
+
return parsed.learnings.map((item) => ({
|
|
331
|
+
id: kbManager.generateLearningId(item.learning),
|
|
332
|
+
category: item.category,
|
|
333
|
+
subcategory: item.subcategory,
|
|
334
|
+
learning: item.learning,
|
|
335
|
+
filePatterns: item.filePatterns,
|
|
336
|
+
sourceInfo: {
|
|
337
|
+
prId,
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.warn(" ⚠️ Failed to parse structured output:", error.message);
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Private Methods
|
|
349
|
+
// ============================================================================
|
|
350
|
+
/**
|
|
351
|
+
* Initialize NeuroLink with observability
|
|
352
|
+
*/
|
|
353
|
+
initializeNeurolink() {
|
|
354
|
+
const observabilityConfig = buildObservabilityConfigFromEnv();
|
|
355
|
+
const neurolinkConfig = {
|
|
356
|
+
conversationMemory: this.config.ai.conversationMemory,
|
|
357
|
+
};
|
|
358
|
+
if (observabilityConfig &&
|
|
359
|
+
validateObservabilityConfig(observabilityConfig)) {
|
|
360
|
+
neurolinkConfig.observability = observabilityConfig;
|
|
361
|
+
}
|
|
362
|
+
return new NeuroLink(neurolinkConfig);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Ensure orchestrator is initialized
|
|
366
|
+
*/
|
|
367
|
+
async ensureInitialized() {
|
|
368
|
+
if (!this.initialized) {
|
|
369
|
+
await this.initialize();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Handle dry run mode
|
|
374
|
+
*/
|
|
375
|
+
handleDryRun(request, learnings) {
|
|
376
|
+
console.log("\n📋 DRY RUN - Extracted learnings preview:\n");
|
|
377
|
+
if (request.outputFormat === "json") {
|
|
378
|
+
console.log(JSON.stringify({
|
|
379
|
+
prId: request.pullRequestId,
|
|
380
|
+
learningsFound: learnings.length,
|
|
381
|
+
learnings: learnings.map((l) => ({
|
|
382
|
+
category: l.category,
|
|
383
|
+
subcategory: l.subcategory,
|
|
384
|
+
learning: l.learning,
|
|
385
|
+
})),
|
|
386
|
+
}, null, 2));
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// Markdown format
|
|
390
|
+
const byCategory = new Map();
|
|
391
|
+
for (const learning of learnings) {
|
|
392
|
+
const cat = learning.category;
|
|
393
|
+
if (!byCategory.has(cat)) {
|
|
394
|
+
byCategory.set(cat, []);
|
|
395
|
+
}
|
|
396
|
+
byCategory.get(cat).push(learning);
|
|
397
|
+
}
|
|
398
|
+
for (const [category, items] of byCategory) {
|
|
399
|
+
console.log(`## ${category.replace(/_/g, " ").toUpperCase()}`);
|
|
400
|
+
for (const item of items) {
|
|
401
|
+
console.log(`- ${item.learning}`);
|
|
402
|
+
}
|
|
403
|
+
console.log("");
|
|
404
|
+
}
|
|
405
|
+
console.log("─".repeat(60));
|
|
406
|
+
console.log("Run without --dry-run to commit these learnings.");
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
prId: request.pullRequestId,
|
|
411
|
+
learningsFound: learnings.length,
|
|
412
|
+
learningsAdded: 0,
|
|
413
|
+
learningsDuplicate: 0,
|
|
414
|
+
learnings,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Run AI-powered summarization
|
|
419
|
+
*/
|
|
420
|
+
async runSummarization(kbManager) {
|
|
421
|
+
const currentContent = await kbManager.getForPrompt();
|
|
422
|
+
if (!currentContent) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const systemPrompt = await this.promptManager.getSummarizationPrompt();
|
|
426
|
+
const prompt = `
|
|
427
|
+
${systemPrompt}
|
|
428
|
+
|
|
429
|
+
<current-knowledge-base>
|
|
430
|
+
${currentContent}
|
|
431
|
+
</current-knowledge-base>
|
|
432
|
+
|
|
433
|
+
Consolidate the learnings as instructed. Return the complete updated knowledge base in markdown format.
|
|
434
|
+
`;
|
|
435
|
+
const response = await this.neurolink.generate({
|
|
436
|
+
input: { text: prompt },
|
|
437
|
+
provider: this.config.ai.provider,
|
|
438
|
+
model: this.config.ai.model,
|
|
439
|
+
temperature: 0.2,
|
|
440
|
+
maxTokens: 30000,
|
|
441
|
+
});
|
|
442
|
+
// Parse and update the knowledge base
|
|
443
|
+
const newContent = (response.content || "");
|
|
444
|
+
// Extract markdown content (may be in code block)
|
|
445
|
+
let markdownContent = newContent;
|
|
446
|
+
const mdMatch = newContent.match(/```(?:markdown|md)?\s*([\s\S]*?)```/);
|
|
447
|
+
if (mdMatch) {
|
|
448
|
+
markdownContent = mdMatch[1].trim();
|
|
449
|
+
}
|
|
450
|
+
// Verify it looks like valid markdown KB
|
|
451
|
+
if (markdownContent.includes("# Project Knowledge Base")) {
|
|
452
|
+
// Write the summarized content directly to file
|
|
453
|
+
await kbManager.writeRaw(markdownContent);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create empty result for cases with no learnings
|
|
458
|
+
*/
|
|
459
|
+
createEmptyResult(request, reason) {
|
|
460
|
+
console.log(`\n⚠️ ${reason}\n`);
|
|
461
|
+
return {
|
|
462
|
+
success: true,
|
|
463
|
+
prId: request.pullRequestId,
|
|
464
|
+
learningsFound: 0,
|
|
465
|
+
learningsAdded: 0,
|
|
466
|
+
learningsDuplicate: 0,
|
|
467
|
+
learnings: [],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Log the final result
|
|
472
|
+
*/
|
|
473
|
+
logResult(result) {
|
|
474
|
+
console.log("\n" + "═".repeat(60));
|
|
475
|
+
console.log("✅ Learning Extraction Complete");
|
|
476
|
+
console.log("═".repeat(60));
|
|
477
|
+
console.log(` PR: #${result.prId}`);
|
|
478
|
+
console.log(` Learnings found: ${result.learningsFound}`);
|
|
479
|
+
console.log(` Learnings added: ${result.learningsAdded}`);
|
|
480
|
+
console.log(` Duplicates skipped: ${result.learningsDuplicate}`);
|
|
481
|
+
if (result.knowledgeBasePath) {
|
|
482
|
+
console.log(` Knowledge base: ${result.knowledgeBasePath}`);
|
|
483
|
+
}
|
|
484
|
+
if (result.committed) {
|
|
485
|
+
console.log(` 📤 Changes committed`);
|
|
486
|
+
}
|
|
487
|
+
if (result.summarized) {
|
|
488
|
+
console.log(` 🔄 Knowledge base summarized`);
|
|
489
|
+
}
|
|
490
|
+
console.log("═".repeat(60) + "\n");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Factory function to create LearningOrchestrator
|
|
495
|
+
*/
|
|
496
|
+
export function createLearningOrchestrator() {
|
|
497
|
+
return new LearningOrchestrator();
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=LearningOrchestrator.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Manager for Yama V2
|
|
3
|
+
* Manages lifecycle and health of Bitbucket and Jira MCP servers
|
|
4
|
+
*/
|
|
5
|
+
import { MCPServersConfig } from "../types/config.types.js";
|
|
6
|
+
export declare class MCPServerManager {
|
|
7
|
+
private initialized;
|
|
8
|
+
/**
|
|
9
|
+
* Setup all MCP servers in NeuroLink
|
|
10
|
+
* Bitbucket is always enabled, Jira is optional based on config
|
|
11
|
+
*/
|
|
12
|
+
setupMCPServers(neurolink: any, config: MCPServersConfig): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Setup Bitbucket MCP server (hardcoded, always enabled)
|
|
15
|
+
*/
|
|
16
|
+
private setupBitbucketMCP;
|
|
17
|
+
/**
|
|
18
|
+
* Setup Jira MCP server (hardcoded, optionally enabled)
|
|
19
|
+
*/
|
|
20
|
+
private setupJiraMCP;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=MCPServerManager.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server Manager for Yama V2
|
|
3
|
+
* Manages lifecycle and health of Bitbucket and Jira MCP servers
|
|
4
|
+
*/
|
|
5
|
+
import { MCPServerError } from "../types/v2.types.js";
|
|
6
|
+
export class MCPServerManager {
|
|
7
|
+
// MCP servers are managed entirely by NeuroLink
|
|
8
|
+
// No need to track tools locally
|
|
9
|
+
initialized = false;
|
|
10
|
+
/**
|
|
11
|
+
* Setup all MCP servers in NeuroLink
|
|
12
|
+
* Bitbucket is always enabled, Jira is optional based on config
|
|
13
|
+
*/
|
|
14
|
+
async setupMCPServers(neurolink, config) {
|
|
15
|
+
console.log("🔌 Setting up MCP servers...");
|
|
16
|
+
// Setup Bitbucket MCP (always enabled)
|
|
17
|
+
await this.setupBitbucketMCP(neurolink, config.bitbucket?.blockedTools);
|
|
18
|
+
// Setup Jira MCP (optional)
|
|
19
|
+
if (config.jira.enabled) {
|
|
20
|
+
await this.setupJiraMCP(neurolink, config.jira.blockedTools);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.log(" ⏭️ Jira MCP disabled in config");
|
|
24
|
+
}
|
|
25
|
+
this.initialized = true;
|
|
26
|
+
console.log("✅ MCP servers configured\n");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Setup Bitbucket MCP server (hardcoded, always enabled)
|
|
30
|
+
*/
|
|
31
|
+
async setupBitbucketMCP(neurolink, blockedTools) {
|
|
32
|
+
try {
|
|
33
|
+
console.log(" 🔧 Registering Bitbucket MCP server...");
|
|
34
|
+
// Verify environment variables
|
|
35
|
+
if (!process.env.BITBUCKET_USERNAME ||
|
|
36
|
+
!process.env.BITBUCKET_TOKEN ||
|
|
37
|
+
!process.env.BITBUCKET_BASE_URL) {
|
|
38
|
+
throw new MCPServerError("Missing required environment variables: BITBUCKET_USERNAME, BITBUCKET_TOKEN, or BITBUCKET_BASE_URL");
|
|
39
|
+
}
|
|
40
|
+
// Hardcoded Bitbucket MCP configuration
|
|
41
|
+
await neurolink.addExternalMCPServer("bitbucket", {
|
|
42
|
+
command: "npx",
|
|
43
|
+
args: ["-y", "@nexus2520/bitbucket-mcp-server"],
|
|
44
|
+
transport: "stdio",
|
|
45
|
+
env: {
|
|
46
|
+
BITBUCKET_USERNAME: process.env.BITBUCKET_USERNAME,
|
|
47
|
+
BITBUCKET_TOKEN: process.env.BITBUCKET_TOKEN,
|
|
48
|
+
BITBUCKET_BASE_URL: process.env.BITBUCKET_BASE_URL,
|
|
49
|
+
},
|
|
50
|
+
blockedTools: blockedTools || [],
|
|
51
|
+
});
|
|
52
|
+
console.log(" ✅ Bitbucket MCP server registered and tools available");
|
|
53
|
+
if (blockedTools && blockedTools.length > 0) {
|
|
54
|
+
console.log(` 🚫 Blocked tools: ${blockedTools.join(", ")}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new MCPServerError(`Failed to setup Bitbucket MCP server: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Setup Jira MCP server (hardcoded, optionally enabled)
|
|
63
|
+
*/
|
|
64
|
+
async setupJiraMCP(neurolink, blockedTools) {
|
|
65
|
+
try {
|
|
66
|
+
console.log(" 🔧 Registering Jira MCP server...");
|
|
67
|
+
// Validate required Jira environment variables
|
|
68
|
+
const jiraEmail = process.env.JIRA_EMAIL;
|
|
69
|
+
const jiraToken = process.env.JIRA_API_TOKEN;
|
|
70
|
+
const jiraBaseUrl = process.env.JIRA_BASE_URL;
|
|
71
|
+
if (!jiraEmail || !jiraToken || !jiraBaseUrl) {
|
|
72
|
+
console.warn(" ⚠️ Missing Jira environment variables (JIRA_EMAIL, JIRA_API_TOKEN, or JIRA_BASE_URL)");
|
|
73
|
+
console.warn(" Skipping Jira integration...");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Hardcoded Jira MCP configuration
|
|
77
|
+
await neurolink.addExternalMCPServer("jira", {
|
|
78
|
+
command: "npx",
|
|
79
|
+
args: ["-y", "@nexus2520/jira-mcp-server"],
|
|
80
|
+
transport: "stdio",
|
|
81
|
+
env: {
|
|
82
|
+
JIRA_EMAIL: process.env.JIRA_EMAIL,
|
|
83
|
+
JIRA_API_TOKEN: process.env.JIRA_API_TOKEN,
|
|
84
|
+
JIRA_BASE_URL: process.env.JIRA_BASE_URL,
|
|
85
|
+
},
|
|
86
|
+
blockedTools: blockedTools || [],
|
|
87
|
+
});
|
|
88
|
+
console.log(" ✅ Jira MCP server registered and tools available");
|
|
89
|
+
if (blockedTools && blockedTools.length > 0) {
|
|
90
|
+
console.log(` 🚫 Blocked tools: ${blockedTools.join(", ")}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Jira is optional, so we warn instead of throwing
|
|
95
|
+
console.warn(` ⚠️ Failed to setup Jira MCP server: ${error.message}`);
|
|
96
|
+
console.warn(" Continuing without Jira integration...");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=MCPServerManager.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager for Yama V2
|
|
3
|
+
* Tracks review sessions, tool calls, and maintains state
|
|
4
|
+
*/
|
|
5
|
+
import { ReviewSession, ReviewRequest, ReviewResult, SessionMetadata } from "../types/v2.types.js";
|
|
6
|
+
export declare class SessionManager {
|
|
7
|
+
private sessions;
|
|
8
|
+
private maxSessions;
|
|
9
|
+
/**
|
|
10
|
+
* Create a new review session
|
|
11
|
+
*/
|
|
12
|
+
createSession(request: ReviewRequest): string;
|
|
13
|
+
/**
|
|
14
|
+
* Get session by ID
|
|
15
|
+
*/
|
|
16
|
+
getSession(sessionId: string): ReviewSession;
|
|
17
|
+
/**
|
|
18
|
+
* Record a tool call in the session
|
|
19
|
+
*/
|
|
20
|
+
recordToolCall(sessionId: string, toolName: string, args: any, result: any, duration: number, error?: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Update session metadata
|
|
23
|
+
*/
|
|
24
|
+
updateMetadata(sessionId: string, updates: Partial<SessionMetadata>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Mark session as completed
|
|
27
|
+
*/
|
|
28
|
+
completeSession(sessionId: string, result: ReviewResult): void;
|
|
29
|
+
/**
|
|
30
|
+
* Mark session as failed
|
|
31
|
+
*/
|
|
32
|
+
failSession(sessionId: string, error: Error): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get all active sessions
|
|
35
|
+
*/
|
|
36
|
+
getActiveSessions(): ReviewSession[];
|
|
37
|
+
/**
|
|
38
|
+
* Get session statistics
|
|
39
|
+
*/
|
|
40
|
+
getSessionStats(sessionId: string): {
|
|
41
|
+
duration: number;
|
|
42
|
+
toolCallCount: number;
|
|
43
|
+
uniqueTools: number;
|
|
44
|
+
averageToolCallDuration: number;
|
|
45
|
+
toolCallsByName: Record<string, number>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Generate unique session ID
|
|
49
|
+
*/
|
|
50
|
+
private generateSessionId;
|
|
51
|
+
/**
|
|
52
|
+
* Clean up old sessions (keep most recent 100)
|
|
53
|
+
*/
|
|
54
|
+
private cleanupOldSessions;
|
|
55
|
+
/**
|
|
56
|
+
* Export session data for debugging
|
|
57
|
+
*/
|
|
58
|
+
exportSession(sessionId: string): any;
|
|
59
|
+
/**
|
|
60
|
+
* Summarize tool result for logging
|
|
61
|
+
*/
|
|
62
|
+
private summarizeToolResult;
|
|
63
|
+
/**
|
|
64
|
+
* Clear all sessions
|
|
65
|
+
*/
|
|
66
|
+
clearAll(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Get session count
|
|
69
|
+
*/
|
|
70
|
+
getSessionCount(): number;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=SessionManager.d.ts.map
|