@juspay/yama 2.2.0 → 2.2.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +59 -110
- package/dist/cli/cli.d.ts +13 -0
- package/dist/cli/cli.js +341 -0
- package/dist/cli/v2.cli.d.ts +2 -11
- package/dist/cli/v2.cli.js +4 -354
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/v2/config/ConfigLoader.d.ts +5 -5
- package/dist/v2/config/ConfigLoader.js +42 -24
- package/dist/v2/config/DefaultConfig.d.ts +3 -3
- package/dist/v2/config/DefaultConfig.js +4 -2
- package/dist/v2/core/LocalDiffSource.d.ts +26 -0
- package/dist/v2/core/LocalDiffSource.js +129 -0
- package/dist/v2/core/MCPServerManager.d.ts +13 -1
- package/dist/v2/core/MCPServerManager.js +111 -7
- package/dist/v2/core/SessionManager.d.ts +1 -1
- package/dist/v2/core/SessionManager.js +3 -3
- package/dist/v2/core/YamaV2Orchestrator.d.ts +48 -8
- package/dist/v2/core/YamaV2Orchestrator.js +517 -49
- package/dist/v2/prompts/PromptBuilder.d.ts +10 -4
- package/dist/v2/prompts/PromptBuilder.js +94 -3
- package/dist/v2/prompts/ReviewSystemPrompt.d.ts +1 -1
- package/dist/v2/prompts/ReviewSystemPrompt.js +7 -5
- package/dist/v2/types/config.types.d.ts +10 -1
- package/dist/v2/types/v2.types.d.ts +66 -5
- package/dist/v2/types/v2.types.js +8 -6
- package/package.json +20 -15
- package/yama.config.example.yaml +8 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Yama
|
|
2
|
+
* Yama Orchestrator
|
|
3
3
|
* Main entry point for AI-native autonomous code review
|
|
4
4
|
*/
|
|
5
5
|
import { NeuroLink } from "@juspay/neurolink";
|
|
@@ -7,44 +7,57 @@ import { MCPServerManager } from "./MCPServerManager.js";
|
|
|
7
7
|
import { ConfigLoader } from "../config/ConfigLoader.js";
|
|
8
8
|
import { PromptBuilder } from "../prompts/PromptBuilder.js";
|
|
9
9
|
import { SessionManager } from "./SessionManager.js";
|
|
10
|
+
import { LocalDiffSource } from "./LocalDiffSource.js";
|
|
10
11
|
import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
|
|
11
|
-
export class
|
|
12
|
+
export class YamaOrchestrator {
|
|
12
13
|
neurolink;
|
|
13
14
|
mcpManager;
|
|
14
15
|
configLoader;
|
|
15
16
|
promptBuilder;
|
|
16
17
|
sessionManager;
|
|
18
|
+
localDiffSource;
|
|
17
19
|
config;
|
|
18
20
|
initialized = false;
|
|
19
|
-
|
|
21
|
+
mcpInitialized = false;
|
|
22
|
+
localGitMcpInitialized = false;
|
|
23
|
+
initOptions;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.initOptions = options;
|
|
20
26
|
this.configLoader = new ConfigLoader();
|
|
21
27
|
this.mcpManager = new MCPServerManager();
|
|
22
28
|
this.promptBuilder = new PromptBuilder();
|
|
23
29
|
this.sessionManager = new SessionManager();
|
|
30
|
+
this.localDiffSource = new LocalDiffSource();
|
|
24
31
|
}
|
|
25
32
|
/**
|
|
26
|
-
* Initialize Yama
|
|
33
|
+
* Initialize Yama with configuration and MCP servers
|
|
27
34
|
*/
|
|
28
|
-
async initialize(configPath) {
|
|
29
|
-
if (this.initialized) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
this.showBanner();
|
|
33
|
-
console.log("🚀 Initializing Yama V2...\n");
|
|
35
|
+
async initialize(configPath, mode = "pr") {
|
|
34
36
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
if (!this.initialized) {
|
|
38
|
+
console.log("🚀 Initializing Yama...\n");
|
|
39
|
+
// Step 1: Load configuration with SDK-style instance overrides
|
|
40
|
+
const resolvedConfigPath = configPath || this.initOptions.configPath;
|
|
41
|
+
this.config = await this.configLoader.loadConfig(resolvedConfigPath, this.initOptions.configOverrides);
|
|
42
|
+
this.showBanner();
|
|
43
|
+
// Step 2: Initialize NeuroLink
|
|
44
|
+
console.log("🧠 Initializing NeuroLink AI engine...");
|
|
45
|
+
this.neurolink = this.initializeNeurolink();
|
|
46
|
+
console.log("✅ NeuroLink initialized\n");
|
|
47
|
+
this.initialized = true;
|
|
48
|
+
}
|
|
49
|
+
// Step 3: Mode-specific setup
|
|
50
|
+
if (mode === "pr" && !this.mcpInitialized) {
|
|
51
|
+
await this.mcpManager.setupMCPServers(this.neurolink, this.config.mcpServers);
|
|
52
|
+
this.mcpInitialized = true;
|
|
53
|
+
}
|
|
54
|
+
else if (mode === "local" && !this.localGitMcpInitialized) {
|
|
55
|
+
await this.mcpManager.setupLocalGitMCPServer(this.neurolink);
|
|
56
|
+
this.localGitMcpInitialized = true;
|
|
57
|
+
}
|
|
58
|
+
// Step 4: Mode-specific validation
|
|
59
|
+
await this.configLoader.validate(mode);
|
|
60
|
+
console.log("✅ Yama initialized successfully\n");
|
|
48
61
|
console.log("═".repeat(60) + "\n");
|
|
49
62
|
}
|
|
50
63
|
catch (error) {
|
|
@@ -56,7 +69,7 @@ export class YamaV2Orchestrator {
|
|
|
56
69
|
* Start autonomous AI review
|
|
57
70
|
*/
|
|
58
71
|
async startReview(request) {
|
|
59
|
-
await this.ensureInitialized();
|
|
72
|
+
await this.ensureInitialized("pr", request.configPath);
|
|
60
73
|
const startTime = Date.now();
|
|
61
74
|
const sessionId = this.sessionManager.createSession(request);
|
|
62
75
|
this.logReviewStart(request, sessionId);
|
|
@@ -86,6 +99,8 @@ export class YamaV2Orchestrator {
|
|
|
86
99
|
temperature: this.config.ai.temperature,
|
|
87
100
|
maxTokens: this.config.ai.maxTokens,
|
|
88
101
|
timeout: this.config.ai.timeout,
|
|
102
|
+
skipToolPromptInjection: true,
|
|
103
|
+
...this.getPRToolFilteringOptions(instructions),
|
|
89
104
|
context: {
|
|
90
105
|
sessionId,
|
|
91
106
|
userId: this.generateUserId(request),
|
|
@@ -95,6 +110,7 @@ export class YamaV2Orchestrator {
|
|
|
95
110
|
enableAnalytics: this.config.ai.enableAnalytics,
|
|
96
111
|
enableEvaluation: this.config.ai.enableEvaluation,
|
|
97
112
|
});
|
|
113
|
+
this.recordToolCallsFromResponse(sessionId, aiResponse);
|
|
98
114
|
// Extract and parse results
|
|
99
115
|
const result = this.parseReviewResult(aiResponse, startTime, sessionId);
|
|
100
116
|
// Update session with results
|
|
@@ -108,11 +124,83 @@ export class YamaV2Orchestrator {
|
|
|
108
124
|
throw error;
|
|
109
125
|
}
|
|
110
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Unified review entry for SDK consumers.
|
|
129
|
+
*/
|
|
130
|
+
async review(request) {
|
|
131
|
+
if (this.isLocalReviewRequest(request)) {
|
|
132
|
+
return this.reviewLocalDiff(request);
|
|
133
|
+
}
|
|
134
|
+
return this.startReview(request);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Local SDK mode review from git diff (no PR/MCP dependency).
|
|
138
|
+
*/
|
|
139
|
+
async reviewLocalDiff(request) {
|
|
140
|
+
await this.ensureInitialized("local", request.configPath);
|
|
141
|
+
const sessionSeed = request.repoPath || process.cwd();
|
|
142
|
+
const pseudoRequest = {
|
|
143
|
+
mode: "pr",
|
|
144
|
+
workspace: "local",
|
|
145
|
+
repository: sessionSeed.split("/").pop() || "local-repo",
|
|
146
|
+
dryRun: request.dryRun,
|
|
147
|
+
verbose: request.verbose,
|
|
148
|
+
configPath: request.configPath,
|
|
149
|
+
};
|
|
150
|
+
const sessionId = this.sessionManager.createSession(pseudoRequest);
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
const diffContext = this.localDiffSource.getDiffContext(request);
|
|
154
|
+
const instructions = await this.promptBuilder.buildLocalReviewInstructions(request, this.config, diffContext);
|
|
155
|
+
const aiResponse = await this.neurolink.generate({
|
|
156
|
+
input: { text: instructions },
|
|
157
|
+
provider: this.config.ai.provider,
|
|
158
|
+
model: this.config.ai.model,
|
|
159
|
+
temperature: this.config.ai.temperature,
|
|
160
|
+
maxTokens: Math.min(this.config.ai.maxTokens, 16_000),
|
|
161
|
+
maxSteps: 100,
|
|
162
|
+
prepareStep: async ({ stepNumber }) => {
|
|
163
|
+
if (stepNumber >= 5) {
|
|
164
|
+
return { toolChoice: "none" };
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
},
|
|
168
|
+
timeout: this.config.ai.timeout,
|
|
169
|
+
enableAnalytics: this.config.ai.enableAnalytics,
|
|
170
|
+
enableEvaluation: this.config.ai.enableEvaluation,
|
|
171
|
+
// Request JSON output at the provider level (prompt-level mode; safe alongside tools).
|
|
172
|
+
output: { format: "json" },
|
|
173
|
+
// Tools are passed natively; avoids huge duplicated tool-schema prompt injection.
|
|
174
|
+
skipToolPromptInjection: true,
|
|
175
|
+
...this.getLocalToolFilteringOptions(),
|
|
176
|
+
context: {
|
|
177
|
+
sessionId,
|
|
178
|
+
userId: `local-${sessionId}`,
|
|
179
|
+
operation: "local-review",
|
|
180
|
+
metadata: {
|
|
181
|
+
repoPath: diffContext.repoPath,
|
|
182
|
+
diffSource: diffContext.diffSource,
|
|
183
|
+
baseRef: diffContext.baseRef,
|
|
184
|
+
headRef: diffContext.headRef,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
this.recordToolCallsFromResponse(sessionId, aiResponse);
|
|
189
|
+
const result = this.parseLocalReviewResult(aiResponse, sessionId, startTime, request, diffContext);
|
|
190
|
+
// Stored as generic session payload for debugging/export parity.
|
|
191
|
+
this.sessionManager.completeSession(sessionId, result);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
this.sessionManager.failSession(sessionId, error);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
111
199
|
/**
|
|
112
200
|
* Stream review with real-time updates (for verbose mode)
|
|
113
201
|
*/
|
|
114
202
|
async *streamReview(request) {
|
|
115
|
-
await this.ensureInitialized();
|
|
203
|
+
await this.ensureInitialized("pr", request.configPath);
|
|
116
204
|
const sessionId = this.sessionManager.createSession(request);
|
|
117
205
|
try {
|
|
118
206
|
// Build instructions
|
|
@@ -137,6 +225,8 @@ export class YamaV2Orchestrator {
|
|
|
137
225
|
input: { text: instructions },
|
|
138
226
|
provider: this.config.ai.provider,
|
|
139
227
|
model: this.config.ai.model,
|
|
228
|
+
skipToolPromptInjection: true,
|
|
229
|
+
...this.getPRToolFilteringOptions(instructions),
|
|
140
230
|
context: {
|
|
141
231
|
sessionId,
|
|
142
232
|
userId: this.generateUserId(request),
|
|
@@ -164,7 +254,7 @@ export class YamaV2Orchestrator {
|
|
|
164
254
|
* This allows the AI to use knowledge gained during review to write better descriptions
|
|
165
255
|
*/
|
|
166
256
|
async startReviewAndEnhance(request) {
|
|
167
|
-
await this.ensureInitialized();
|
|
257
|
+
await this.ensureInitialized("pr", request.configPath);
|
|
168
258
|
const startTime = Date.now();
|
|
169
259
|
const sessionId = this.sessionManager.createSession(request);
|
|
170
260
|
this.logReviewStart(request, sessionId);
|
|
@@ -196,6 +286,8 @@ export class YamaV2Orchestrator {
|
|
|
196
286
|
temperature: this.config.ai.temperature,
|
|
197
287
|
maxTokens: this.config.ai.maxTokens,
|
|
198
288
|
timeout: this.config.ai.timeout,
|
|
289
|
+
skipToolPromptInjection: true,
|
|
290
|
+
...this.getPRToolFilteringOptions(reviewInstructions),
|
|
199
291
|
context: {
|
|
200
292
|
sessionId,
|
|
201
293
|
userId: this.generateUserId(request),
|
|
@@ -205,6 +297,7 @@ export class YamaV2Orchestrator {
|
|
|
205
297
|
enableAnalytics: this.config.ai.enableAnalytics,
|
|
206
298
|
enableEvaluation: this.config.ai.enableEvaluation,
|
|
207
299
|
});
|
|
300
|
+
this.recordToolCallsFromResponse(sessionId, reviewResponse);
|
|
208
301
|
// Parse review results
|
|
209
302
|
const reviewResult = this.parseReviewResult(reviewResponse, startTime, sessionId);
|
|
210
303
|
console.log("\n✅ Phase 1 complete: Code review finished");
|
|
@@ -225,6 +318,8 @@ export class YamaV2Orchestrator {
|
|
|
225
318
|
temperature: this.config.ai.temperature,
|
|
226
319
|
maxTokens: this.config.ai.maxTokens,
|
|
227
320
|
timeout: this.config.ai.timeout,
|
|
321
|
+
skipToolPromptInjection: true,
|
|
322
|
+
...this.getPRToolFilteringOptions(enhanceInstructions),
|
|
228
323
|
context: {
|
|
229
324
|
sessionId, // SAME sessionId = AI remembers review context
|
|
230
325
|
userId: this.generateUserId(request),
|
|
@@ -234,6 +329,7 @@ export class YamaV2Orchestrator {
|
|
|
234
329
|
enableAnalytics: this.config.ai.enableAnalytics,
|
|
235
330
|
enableEvaluation: this.config.ai.enableEvaluation,
|
|
236
331
|
});
|
|
332
|
+
this.recordToolCallsFromResponse(sessionId, enhanceResponse);
|
|
237
333
|
console.log("✅ Phase 2 complete: Description enhanced\n");
|
|
238
334
|
// Add enhancement status to result
|
|
239
335
|
reviewResult.descriptionEnhanced = true;
|
|
@@ -257,7 +353,7 @@ export class YamaV2Orchestrator {
|
|
|
257
353
|
* Enhance PR description only (without full review)
|
|
258
354
|
*/
|
|
259
355
|
async enhanceDescription(request) {
|
|
260
|
-
await this.ensureInitialized();
|
|
356
|
+
await this.ensureInitialized("pr", request.configPath);
|
|
261
357
|
const sessionId = this.sessionManager.createSession(request);
|
|
262
358
|
try {
|
|
263
359
|
console.log("\n📝 Enhancing PR description...\n");
|
|
@@ -268,6 +364,8 @@ export class YamaV2Orchestrator {
|
|
|
268
364
|
input: { text: instructions },
|
|
269
365
|
provider: this.config.ai.provider,
|
|
270
366
|
model: this.config.ai.model,
|
|
367
|
+
skipToolPromptInjection: true,
|
|
368
|
+
...this.getPRToolFilteringOptions(instructions),
|
|
271
369
|
context: {
|
|
272
370
|
sessionId,
|
|
273
371
|
userId: this.generateUserId(request),
|
|
@@ -275,6 +373,7 @@ export class YamaV2Orchestrator {
|
|
|
275
373
|
},
|
|
276
374
|
enableAnalytics: true,
|
|
277
375
|
});
|
|
376
|
+
this.recordToolCallsFromResponse(sessionId, aiResponse);
|
|
278
377
|
console.log("✅ Description enhanced successfully\n");
|
|
279
378
|
return {
|
|
280
379
|
success: true,
|
|
@@ -317,7 +416,7 @@ export class YamaV2Orchestrator {
|
|
|
317
416
|
branch: request.branch,
|
|
318
417
|
dryRun: request.dryRun || false,
|
|
319
418
|
metadata: {
|
|
320
|
-
yamaVersion: "2.
|
|
419
|
+
yamaVersion: "2.2.1",
|
|
321
420
|
startTime: new Date().toISOString(),
|
|
322
421
|
},
|
|
323
422
|
};
|
|
@@ -333,34 +432,321 @@ export class YamaV2Orchestrator {
|
|
|
333
432
|
// Calculate statistics from session tool calls
|
|
334
433
|
const statistics = this.calculateStatistics(session);
|
|
335
434
|
return {
|
|
435
|
+
mode: "pr",
|
|
336
436
|
prId: session.request.pullRequestId || 0,
|
|
337
437
|
decision,
|
|
338
438
|
statistics,
|
|
339
439
|
summary: this.extractSummary(aiResponse),
|
|
340
440
|
duration,
|
|
341
441
|
tokenUsage: {
|
|
342
|
-
input: aiResponse
|
|
343
|
-
output: aiResponse
|
|
344
|
-
total: aiResponse
|
|
442
|
+
input: this.toSafeNumber(aiResponse?.usage?.inputTokens),
|
|
443
|
+
output: this.toSafeNumber(aiResponse?.usage?.outputTokens),
|
|
444
|
+
total: this.toSafeNumber(aiResponse?.usage?.totalTokens),
|
|
345
445
|
},
|
|
346
446
|
costEstimate: this.calculateCost(aiResponse.usage),
|
|
347
447
|
sessionId,
|
|
348
448
|
};
|
|
349
449
|
}
|
|
450
|
+
recordToolCallsFromResponse(sessionId, aiResponse) {
|
|
451
|
+
const toolCalls = Array.isArray(aiResponse?.toolCalls)
|
|
452
|
+
? aiResponse.toolCalls
|
|
453
|
+
: [];
|
|
454
|
+
const toolResults = Array.isArray(aiResponse?.toolResults)
|
|
455
|
+
? aiResponse.toolResults
|
|
456
|
+
: [];
|
|
457
|
+
for (const call of toolCalls) {
|
|
458
|
+
const toolName = call?.toolName || call?.name || call?.tool || "unknown_tool";
|
|
459
|
+
const args = call?.parameters || call?.args || {};
|
|
460
|
+
const matchingResult = toolResults.find((result) => result?.toolCallId === call?.id || result?.toolName === toolName) || null;
|
|
461
|
+
this.sessionManager.recordToolCall(sessionId, toolName, args, matchingResult, 0, matchingResult?.error);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Parse local SDK mode response into strict LocalReviewResult.
|
|
466
|
+
*/
|
|
467
|
+
parseLocalReviewResult(aiResponse, sessionId, startTime, request, diffContext) {
|
|
468
|
+
const rawContent = aiResponse?.content || aiResponse?.outputs?.text || "";
|
|
469
|
+
const parsed = this.extractJsonPayload(rawContent);
|
|
470
|
+
const usage = aiResponse?.usage || {};
|
|
471
|
+
const tokenUsage = {
|
|
472
|
+
input: this.toSafeNumber(usage.inputTokens),
|
|
473
|
+
output: this.toSafeNumber(usage.outputTokens),
|
|
474
|
+
total: this.toSafeNumber(usage.totalTokens),
|
|
475
|
+
};
|
|
476
|
+
if (!parsed) {
|
|
477
|
+
const fallbackIssue = {
|
|
478
|
+
id: "OUTPUT_FORMAT_VIOLATION",
|
|
479
|
+
severity: "MAJOR",
|
|
480
|
+
category: "review-engine",
|
|
481
|
+
title: "Model did not return structured JSON",
|
|
482
|
+
description: "Local review response was unstructured (likely tool-call trace or partial output), so findings cannot be trusted.",
|
|
483
|
+
suggestion: "Retry with a tool-calling capable model or reduce review scope (smaller diff / includePaths) to keep responses structured.",
|
|
484
|
+
};
|
|
485
|
+
const issuesBySeverity = this.countFindingsBySeverity([fallbackIssue]);
|
|
486
|
+
return {
|
|
487
|
+
mode: "local",
|
|
488
|
+
decision: "CHANGES_REQUESTED",
|
|
489
|
+
summary: this.sanitizeLocalSummary(rawContent) ||
|
|
490
|
+
"Local review could not produce structured JSON output.",
|
|
491
|
+
issues: [fallbackIssue],
|
|
492
|
+
enhancements: [],
|
|
493
|
+
statistics: {
|
|
494
|
+
filesChanged: diffContext.changedFiles.length,
|
|
495
|
+
additions: diffContext.additions,
|
|
496
|
+
deletions: diffContext.deletions,
|
|
497
|
+
issuesFound: 1,
|
|
498
|
+
enhancementsFound: 0,
|
|
499
|
+
issuesBySeverity,
|
|
500
|
+
},
|
|
501
|
+
duration: Math.round((Date.now() - startTime) / 1000),
|
|
502
|
+
tokenUsage,
|
|
503
|
+
costEstimate: this.calculateCost(aiResponse?.usage),
|
|
504
|
+
sessionId,
|
|
505
|
+
schemaVersion: request.outputSchemaVersion || "1.0",
|
|
506
|
+
metadata: {
|
|
507
|
+
repoPath: diffContext.repoPath,
|
|
508
|
+
diffSource: diffContext.diffSource,
|
|
509
|
+
baseRef: diffContext.baseRef,
|
|
510
|
+
headRef: diffContext.headRef,
|
|
511
|
+
truncated: diffContext.truncated,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const issues = this.normalizeFindings(parsed?.issues, "issue");
|
|
516
|
+
const enhancements = this.normalizeFindings(parsed?.enhancements, "enhancement");
|
|
517
|
+
const issuesBySeverity = this.countFindingsBySeverity(issues);
|
|
518
|
+
const fallbackForTruncatedNoFindings = diffContext.truncated && issues.length === 0 && enhancements.length === 0;
|
|
519
|
+
const decision = fallbackForTruncatedNoFindings
|
|
520
|
+
? "CHANGES_REQUESTED"
|
|
521
|
+
: this.normalizeDecision(parsed?.decision, issuesBySeverity);
|
|
522
|
+
return {
|
|
523
|
+
mode: "local",
|
|
524
|
+
decision,
|
|
525
|
+
summary: this.sanitizeLocalSummary(parsed?.summary) ||
|
|
526
|
+
this.sanitizeLocalSummary(this.extractSummary(aiResponse)) ||
|
|
527
|
+
"Local review completed",
|
|
528
|
+
issues,
|
|
529
|
+
enhancements,
|
|
530
|
+
statistics: {
|
|
531
|
+
filesChanged: diffContext.changedFiles.length,
|
|
532
|
+
additions: diffContext.additions,
|
|
533
|
+
deletions: diffContext.deletions,
|
|
534
|
+
issuesFound: issues.length,
|
|
535
|
+
enhancementsFound: enhancements.length,
|
|
536
|
+
issuesBySeverity,
|
|
537
|
+
},
|
|
538
|
+
duration: Math.round((Date.now() - startTime) / 1000),
|
|
539
|
+
tokenUsage,
|
|
540
|
+
costEstimate: this.calculateCost(aiResponse?.usage),
|
|
541
|
+
sessionId,
|
|
542
|
+
schemaVersion: request.outputSchemaVersion || "1.0",
|
|
543
|
+
metadata: {
|
|
544
|
+
repoPath: diffContext.repoPath,
|
|
545
|
+
diffSource: diffContext.diffSource,
|
|
546
|
+
baseRef: diffContext.baseRef,
|
|
547
|
+
headRef: diffContext.headRef,
|
|
548
|
+
truncated: diffContext.truncated,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
sanitizeLocalSummary(summary) {
|
|
553
|
+
if (typeof summary !== "string") {
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
// Use string splitting instead of backtracking [\s\S]*? regex to avoid ReDoS.
|
|
557
|
+
let result = this.removeDelimitedSections(summary, "<|tool_calls_section_begin|>", "<|tool_calls_section_end|>");
|
|
558
|
+
result = this.removeDelimitedSections(result, "<|tool_call_begin|>", "<|tool_call_end|>");
|
|
559
|
+
return result.replace(/\s+/g, " ").trim();
|
|
560
|
+
}
|
|
561
|
+
removeDelimitedSections(text, open, close) {
|
|
562
|
+
let result = "";
|
|
563
|
+
let pos = 0;
|
|
564
|
+
while (pos < text.length) {
|
|
565
|
+
const start = text.indexOf(open, pos);
|
|
566
|
+
if (start === -1) {
|
|
567
|
+
result += text.slice(pos);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
result += text.slice(pos, start);
|
|
571
|
+
const end = text.indexOf(close, start + open.length);
|
|
572
|
+
pos = end === -1 ? text.length : end + close.length;
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
extractJsonPayload(content) {
|
|
577
|
+
if (!content || typeof content !== "string") {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
const trimmed = content.trim();
|
|
581
|
+
try {
|
|
582
|
+
return JSON.parse(trimmed);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// Fall through to extraction strategies
|
|
586
|
+
}
|
|
587
|
+
// Use indexOf instead of backtracking [\s\S]*? regex to avoid ReDoS.
|
|
588
|
+
const fenceOpen = trimmed.toLowerCase().indexOf("```json");
|
|
589
|
+
if (fenceOpen !== -1) {
|
|
590
|
+
let contentStart = fenceOpen + 7; // length of "```json"
|
|
591
|
+
while (contentStart < trimmed.length &&
|
|
592
|
+
(trimmed[contentStart] === " " ||
|
|
593
|
+
trimmed[contentStart] === "\n" ||
|
|
594
|
+
trimmed[contentStart] === "\r")) {
|
|
595
|
+
contentStart++;
|
|
596
|
+
}
|
|
597
|
+
const fenceClose = trimmed.indexOf("```", contentStart);
|
|
598
|
+
if (fenceClose !== -1) {
|
|
599
|
+
try {
|
|
600
|
+
return JSON.parse(trimmed.slice(contentStart, fenceClose).trim());
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// Continue
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const start = trimmed.indexOf("{");
|
|
608
|
+
const end = trimmed.lastIndexOf("}");
|
|
609
|
+
if (start >= 0 && end > start) {
|
|
610
|
+
try {
|
|
611
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
normalizeFindings(findings, prefix) {
|
|
620
|
+
if (!Array.isArray(findings)) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
return findings
|
|
624
|
+
.map((raw, index) => {
|
|
625
|
+
const value = (raw || {});
|
|
626
|
+
const severity = this.normalizeSeverity(value.severity);
|
|
627
|
+
if (!severity) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
const lineValue = typeof value.line === "number" ? value.line : undefined;
|
|
631
|
+
const finding = {
|
|
632
|
+
id: (typeof value.id === "string" && value.id.trim()) ||
|
|
633
|
+
`${prefix}-${index + 1}`,
|
|
634
|
+
severity,
|
|
635
|
+
category: (typeof value.category === "string" && value.category.trim()) ||
|
|
636
|
+
"general",
|
|
637
|
+
title: (typeof value.title === "string" && value.title.trim()) ||
|
|
638
|
+
"Untitled finding",
|
|
639
|
+
description: (typeof value.description === "string" &&
|
|
640
|
+
value.description.trim()) ||
|
|
641
|
+
"",
|
|
642
|
+
};
|
|
643
|
+
if (typeof value.filePath === "string" && value.filePath.trim()) {
|
|
644
|
+
finding.filePath = value.filePath;
|
|
645
|
+
}
|
|
646
|
+
if (lineValue && lineValue > 0) {
|
|
647
|
+
finding.line = lineValue;
|
|
648
|
+
}
|
|
649
|
+
if (typeof value.suggestion === "string" && value.suggestion.trim()) {
|
|
650
|
+
finding.suggestion = value.suggestion;
|
|
651
|
+
}
|
|
652
|
+
return finding;
|
|
653
|
+
})
|
|
654
|
+
.filter((item) => item !== null);
|
|
655
|
+
}
|
|
656
|
+
normalizeSeverity(severity) {
|
|
657
|
+
if (typeof severity !== "string") {
|
|
658
|
+
return "MINOR";
|
|
659
|
+
}
|
|
660
|
+
const value = severity.toUpperCase();
|
|
661
|
+
if (value === "CRITICAL" ||
|
|
662
|
+
value === "MAJOR" ||
|
|
663
|
+
value === "MINOR" ||
|
|
664
|
+
value === "SUGGESTION") {
|
|
665
|
+
return value;
|
|
666
|
+
}
|
|
667
|
+
return "MINOR";
|
|
668
|
+
}
|
|
669
|
+
countFindingsBySeverity(findings) {
|
|
670
|
+
const counts = {
|
|
671
|
+
critical: 0,
|
|
672
|
+
major: 0,
|
|
673
|
+
minor: 0,
|
|
674
|
+
suggestions: 0,
|
|
675
|
+
};
|
|
676
|
+
for (const finding of findings) {
|
|
677
|
+
if (finding.severity === "CRITICAL") {
|
|
678
|
+
counts.critical += 1;
|
|
679
|
+
}
|
|
680
|
+
else if (finding.severity === "MAJOR") {
|
|
681
|
+
counts.major += 1;
|
|
682
|
+
}
|
|
683
|
+
else if (finding.severity === "MINOR") {
|
|
684
|
+
counts.minor += 1;
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
counts.suggestions += 1;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return counts;
|
|
691
|
+
}
|
|
692
|
+
normalizeDecision(decision, issuesBySeverity) {
|
|
693
|
+
if (typeof decision === "string") {
|
|
694
|
+
const upper = decision.toUpperCase();
|
|
695
|
+
if (upper === "APPROVED" ||
|
|
696
|
+
upper === "CHANGES_REQUESTED" ||
|
|
697
|
+
upper === "BLOCKED") {
|
|
698
|
+
return upper;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (issuesBySeverity.critical > 0) {
|
|
702
|
+
return "BLOCKED";
|
|
703
|
+
}
|
|
704
|
+
if (issuesBySeverity.major + issuesBySeverity.minor > 0) {
|
|
705
|
+
return "CHANGES_REQUESTED";
|
|
706
|
+
}
|
|
707
|
+
return "APPROVED";
|
|
708
|
+
}
|
|
350
709
|
/**
|
|
351
710
|
* Extract decision from AI response
|
|
352
711
|
*/
|
|
353
712
|
extractDecision(aiResponse, session) {
|
|
354
|
-
//
|
|
713
|
+
// Derive final review state from tool calls.
|
|
714
|
+
// Supports both current Bitbucket MCP tools and legacy names.
|
|
355
715
|
const toolCalls = session.toolCalls || [];
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
716
|
+
let requestedChanges;
|
|
717
|
+
let approved;
|
|
718
|
+
for (const tc of toolCalls) {
|
|
719
|
+
const name = tc?.toolName;
|
|
720
|
+
const args = tc?.args || {};
|
|
721
|
+
if (name === "set_review_status") {
|
|
722
|
+
if (typeof args.request_changes === "boolean") {
|
|
723
|
+
requestedChanges = args.request_changes;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else if (name === "request_changes") {
|
|
727
|
+
requestedChanges = true;
|
|
728
|
+
}
|
|
729
|
+
else if (name === "remove_requested_changes") {
|
|
730
|
+
requestedChanges = false;
|
|
731
|
+
}
|
|
732
|
+
else if (name === "set_pr_approval") {
|
|
733
|
+
if (typeof args.approved === "boolean") {
|
|
734
|
+
approved = args.approved;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else if (name === "approve_pull_request") {
|
|
738
|
+
approved = true;
|
|
739
|
+
}
|
|
740
|
+
else if (name === "unapprove_pull_request") {
|
|
741
|
+
approved = false;
|
|
742
|
+
}
|
|
360
743
|
}
|
|
361
|
-
if (
|
|
744
|
+
if (requestedChanges === true) {
|
|
362
745
|
return "BLOCKED";
|
|
363
746
|
}
|
|
747
|
+
if (approved === true) {
|
|
748
|
+
return "APPROVED";
|
|
749
|
+
}
|
|
364
750
|
// Default to changes requested if unclear
|
|
365
751
|
return "CHANGES_REQUESTED";
|
|
366
752
|
}
|
|
@@ -428,9 +814,16 @@ export class YamaV2Orchestrator {
|
|
|
428
814
|
// Rough estimates (update with actual pricing)
|
|
429
815
|
const inputCostPer1M = 0.25; // $0.25 per 1M input tokens (Gemini 2.0 Flash)
|
|
430
816
|
const outputCostPer1M = 1.0; // $1.00 per 1M output tokens
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
|
|
817
|
+
const inputTokens = this.toSafeNumber(usage.inputTokens);
|
|
818
|
+
const outputTokens = this.toSafeNumber(usage.outputTokens);
|
|
819
|
+
const inputCost = (inputTokens / 1_000_000) * inputCostPer1M;
|
|
820
|
+
const outputCost = (outputTokens / 1_000_000) * outputCostPer1M;
|
|
821
|
+
const totalCost = inputCost + outputCost;
|
|
822
|
+
return Number.isFinite(totalCost) ? Number(totalCost.toFixed(4)) : 0;
|
|
823
|
+
}
|
|
824
|
+
toSafeNumber(value) {
|
|
825
|
+
const parsed = Number(value);
|
|
826
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
434
827
|
}
|
|
435
828
|
/**
|
|
436
829
|
* Generate userId for NeuroLink context from repository and branch/PR
|
|
@@ -440,6 +833,79 @@ export class YamaV2Orchestrator {
|
|
|
440
833
|
const identifier = request.branch || `pr-${request.pullRequestId}`;
|
|
441
834
|
return `${repo}-${identifier}`;
|
|
442
835
|
}
|
|
836
|
+
isLocalReviewRequest(request) {
|
|
837
|
+
return (request.mode === "local" ||
|
|
838
|
+
(!("workspace" in request) && !("repository" in request)));
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Query-level tool filtering for PR mode.
|
|
842
|
+
* Conservative strategy: only exclude Jira tools when there is no Jira signal.
|
|
843
|
+
*/
|
|
844
|
+
getPRToolFilteringOptions(inputText) {
|
|
845
|
+
if (!this.config.ai.enableToolFiltering) {
|
|
846
|
+
return {};
|
|
847
|
+
}
|
|
848
|
+
const mode = this.config.ai.toolFilteringMode || "active";
|
|
849
|
+
if (mode === "off") {
|
|
850
|
+
return {};
|
|
851
|
+
}
|
|
852
|
+
const hasJiraSignal = /\b[A-Z]{2,}-\d+\b/.test(inputText) || /\bjira\b/i.test(inputText);
|
|
853
|
+
if (hasJiraSignal) {
|
|
854
|
+
return {};
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
const externalTools = this.neurolink.getExternalMCPTools?.();
|
|
858
|
+
const jiraToolNames = Array.isArray(externalTools)
|
|
859
|
+
? externalTools
|
|
860
|
+
.filter((tool) => tool?.serverId === "jira")
|
|
861
|
+
.map((tool) => tool?.name)
|
|
862
|
+
.filter((name) => typeof name === "string")
|
|
863
|
+
: [];
|
|
864
|
+
if (jiraToolNames.length === 0) {
|
|
865
|
+
return {};
|
|
866
|
+
}
|
|
867
|
+
if (mode === "log-only") {
|
|
868
|
+
console.log(` [tool-filter] log-only: would exclude ${jiraToolNames.length} Jira tools`);
|
|
869
|
+
return {};
|
|
870
|
+
}
|
|
871
|
+
return { excludeTools: jiraToolNames };
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
// Non-fatal: fallback to all tools.
|
|
875
|
+
return {};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Query-level read-only filtering for local-git MCP tools.
|
|
880
|
+
* Uses regex-based mutation detection instead of hardcoded tool-name lists.
|
|
881
|
+
*/
|
|
882
|
+
getLocalToolFilteringOptions() {
|
|
883
|
+
try {
|
|
884
|
+
const externalTools = this.neurolink.getExternalMCPTools?.();
|
|
885
|
+
if (!Array.isArray(externalTools)) {
|
|
886
|
+
return {};
|
|
887
|
+
}
|
|
888
|
+
const mutatingGitToolPattern = /^git_(commit|push|add|checkout|create_branch|merge|rebase|cherry_pick|reset|revert|tag|rm|clean|stash|apply)\b/i;
|
|
889
|
+
// High-volume read operations can flood context with huge payloads.
|
|
890
|
+
const highVolumeGitToolPattern = /^git_(diff|diff_staged|diff_unstaged|log|show)\b/i;
|
|
891
|
+
const excludeTools = externalTools
|
|
892
|
+
.filter((tool) => tool?.serverId === "local-git")
|
|
893
|
+
.map((tool) => tool?.name)
|
|
894
|
+
.filter((name) => typeof name === "string")
|
|
895
|
+
.filter((name) => mutatingGitToolPattern.test(this.normalizeToolName(name)) ||
|
|
896
|
+
highVolumeGitToolPattern.test(this.normalizeToolName(name)));
|
|
897
|
+
if (excludeTools.length === 0) {
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
return { excludeTools };
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return {};
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
normalizeToolName(name) {
|
|
907
|
+
return name.split(/[.:/]/).pop() || name;
|
|
908
|
+
}
|
|
443
909
|
/**
|
|
444
910
|
* Initialize NeuroLink with observability configuration
|
|
445
911
|
*/
|
|
@@ -471,13 +937,11 @@ export class YamaV2Orchestrator {
|
|
|
471
937
|
/**
|
|
472
938
|
* Ensure orchestrator is initialized
|
|
473
939
|
*/
|
|
474
|
-
async ensureInitialized() {
|
|
475
|
-
|
|
476
|
-
await this.initialize();
|
|
477
|
-
}
|
|
940
|
+
async ensureInitialized(mode = "pr", configPath) {
|
|
941
|
+
await this.initialize(configPath, mode);
|
|
478
942
|
}
|
|
479
943
|
/**
|
|
480
|
-
* Show Yama
|
|
944
|
+
* Show Yama banner
|
|
481
945
|
*/
|
|
482
946
|
showBanner() {
|
|
483
947
|
if (!this.config?.display?.showBanner) {
|
|
@@ -485,9 +949,9 @@ export class YamaV2Orchestrator {
|
|
|
485
949
|
}
|
|
486
950
|
console.log("\n" + "═".repeat(60));
|
|
487
951
|
console.log(`
|
|
488
|
-
⚔️ YAMA
|
|
952
|
+
⚔️ YAMA - AI-Native Code Review Guardian
|
|
489
953
|
|
|
490
|
-
Version: 2.
|
|
954
|
+
Version: 2.2.1
|
|
491
955
|
Mode: Autonomous AI-Powered Review
|
|
492
956
|
Powered by: NeuroLink + MCP Tools
|
|
493
957
|
`);
|
|
@@ -543,7 +1007,11 @@ export class YamaV2Orchestrator {
|
|
|
543
1007
|
}
|
|
544
1008
|
}
|
|
545
1009
|
// Export factory function
|
|
546
|
-
export function createYamaV2() {
|
|
547
|
-
return
|
|
1010
|
+
export function createYamaV2(options = {}) {
|
|
1011
|
+
return createYama(options);
|
|
1012
|
+
}
|
|
1013
|
+
export function createYama(options = {}) {
|
|
1014
|
+
return new YamaOrchestrator(options);
|
|
548
1015
|
}
|
|
1016
|
+
export { YamaOrchestrator as YamaV2Orchestrator };
|
|
549
1017
|
//# sourceMappingURL=YamaV2Orchestrator.js.map
|