@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Yama V2 Orchestrator
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 YamaV2Orchestrator {
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
- constructor() {
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 V2 with configuration and MCP servers
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
- // Step 1: Load configuration
36
- this.config = await this.configLoader.loadConfig(configPath);
37
- // Step 2: Initialize NeuroLink with observability
38
- console.log("🧠 Initializing NeuroLink AI engine...");
39
- this.neurolink = this.initializeNeurolink();
40
- console.log("✅ NeuroLink initialized\n");
41
- // Step 3: Setup MCP servers
42
- await this.mcpManager.setupMCPServers(this.neurolink, this.config.mcpServers);
43
- console.log("✅ MCP servers ready (tools managed by NeuroLink)\n");
44
- // Step 4: Validate configuration
45
- await this.configLoader.validate();
46
- this.initialized = true;
47
- console.log("✅ Yama V2 initialized successfully\n");
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.0.0",
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.usage?.inputTokens || 0,
343
- output: aiResponse.usage?.outputTokens || 0,
344
- total: aiResponse.usage?.totalTokens || 0,
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
- // Check if AI called approve_pull_request or request_changes
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
- const approveCall = toolCalls.find((tc) => tc.toolName === "approve_pull_request");
357
- const requestChangesCall = toolCalls.find((tc) => tc.toolName === "request_changes");
358
- if (approveCall) {
359
- return "APPROVED";
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 (requestChangesCall) {
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 inputCost = (usage.inputTokens / 1_000_000) * inputCostPer1M;
432
- const outputCost = (usage.outputTokens / 1_000_000) * outputCostPer1M;
433
- return Number((inputCost + outputCost).toFixed(4));
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
- if (!this.initialized) {
476
- await this.initialize();
477
- }
940
+ async ensureInitialized(mode = "pr", configPath) {
941
+ await this.initialize(configPath, mode);
478
942
  }
479
943
  /**
480
- * Show Yama V2 banner
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 V2 - AI-Native Code Review Guardian
952
+ ⚔️ YAMA - AI-Native Code Review Guardian
489
953
 
490
- Version: 2.0.0
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 new YamaV2Orchestrator();
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