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