@juspay/yama 2.4.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/cli/cli.js +74 -3
- package/dist/index.js +4 -0
- package/dist/v2/config/ConfigLoader.d.ts +7 -2
- package/dist/v2/config/ConfigLoader.js +29 -10
- package/dist/v2/config/DefaultConfig.js +13 -0
- package/dist/v2/core/LearningOrchestrator.js +21 -5
- package/dist/v2/core/MCPServerManager.d.ts +36 -3
- package/dist/v2/core/MCPServerManager.js +173 -6
- package/dist/v2/core/YamaV2Orchestrator.d.ts +20 -1
- package/dist/v2/core/YamaV2Orchestrator.js +168 -58
- package/dist/v2/exploration/ContextExplorerService.d.ts +9 -0
- package/dist/v2/exploration/ContextExplorerService.js +50 -9
- package/dist/v2/exploration/types.d.ts +1 -0
- package/dist/v2/learning/types.d.ts +2 -0
- package/dist/v2/memory/MemoryManager.d.ts +20 -4
- package/dist/v2/memory/MemoryManager.js +30 -5
- package/dist/v2/prompts/PromptBuilder.d.ts +10 -2
- package/dist/v2/prompts/PromptBuilder.js +70 -59
- package/dist/v2/prompts/ReviewSystemPrompt.d.ts +29 -1
- package/dist/v2/prompts/ReviewSystemPrompt.js +34 -43
- package/dist/v2/providers/ProviderToolset.d.ts +54 -0
- package/dist/v2/providers/ProviderToolset.js +389 -0
- package/dist/v2/types/config.types.d.ts +40 -9
- package/dist/v2/types/mcp.types.d.ts +105 -193
- package/dist/v2/types/mcp.types.js +9 -2
- package/dist/v2/types/v2.types.d.ts +7 -2
- package/dist/v2/utils/ProviderDetector.d.ts +57 -0
- package/dist/v2/utils/ProviderDetector.js +187 -0
- package/package.json +2 -1
|
@@ -10,6 +10,8 @@ import { SessionManager } from "./SessionManager.js";
|
|
|
10
10
|
import { MemoryManager } from "../memory/MemoryManager.js";
|
|
11
11
|
import { LocalDiffSource } from "./LocalDiffSource.js";
|
|
12
12
|
import { ContextExplorerService } from "../exploration/ContextExplorerService.js";
|
|
13
|
+
import { ProviderDetector, } from "../utils/ProviderDetector.js";
|
|
14
|
+
import { getProviderToolset } from "../providers/ProviderToolset.js";
|
|
13
15
|
import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
|
|
14
16
|
export class YamaOrchestrator {
|
|
15
17
|
neurolink;
|
|
@@ -23,11 +25,16 @@ export class YamaOrchestrator {
|
|
|
23
25
|
config;
|
|
24
26
|
initialized = false;
|
|
25
27
|
mcpInitialized = false;
|
|
28
|
+
// Provider the PR-mode MCP servers were last set up for. Lets one orchestrator
|
|
29
|
+
// instance switch providers mid-process (Bitbucket PR then GitHub PR) by
|
|
30
|
+
// re-running setup when the detected provider no longer matches.
|
|
31
|
+
mcpProvider = null;
|
|
26
32
|
localGitMcpInitialized = false;
|
|
27
33
|
exploreToolRegistered = false;
|
|
28
34
|
currentToolContext = null;
|
|
29
35
|
initOptions;
|
|
30
36
|
bootstrapStandardsCache = new Map();
|
|
37
|
+
detectedProvider = "bitbucket";
|
|
31
38
|
constructor(options = {}) {
|
|
32
39
|
this.initOptions = options;
|
|
33
40
|
this.configLoader = new ConfigLoader();
|
|
@@ -39,7 +46,7 @@ export class YamaOrchestrator {
|
|
|
39
46
|
/**
|
|
40
47
|
* Initialize Yama with configuration and MCP servers
|
|
41
48
|
*/
|
|
42
|
-
async initialize(configPath, mode = "pr") {
|
|
49
|
+
async initialize(configPath, mode = "pr", request) {
|
|
43
50
|
try {
|
|
44
51
|
if (!this.initialized) {
|
|
45
52
|
console.log("🚀 Initializing Yama...\n");
|
|
@@ -47,6 +54,22 @@ export class YamaOrchestrator {
|
|
|
47
54
|
const resolvedConfigPath = configPath || this.initOptions.configPath;
|
|
48
55
|
this.config = await this.configLoader.loadConfig(resolvedConfigPath, this.initOptions.configOverrides);
|
|
49
56
|
this.showBanner();
|
|
57
|
+
// Step 1.5: Detect provider BEFORE MCP setup so the correct provider's
|
|
58
|
+
// MCP server is wired up. When the real request is threaded in, detect
|
|
59
|
+
// from it; otherwise fall back to a preliminary detection (for logging
|
|
60
|
+
// and env/config-default driven runs).
|
|
61
|
+
if (request) {
|
|
62
|
+
this.detectedProvider = ProviderDetector.detect(request, process.env, this.config.defaultProvider);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const preliminaryRequest = {
|
|
66
|
+
mode: "pr",
|
|
67
|
+
workspace: "",
|
|
68
|
+
repository: "",
|
|
69
|
+
};
|
|
70
|
+
this.detectedProvider = ProviderDetector.detect(preliminaryRequest, process.env, this.config.defaultProvider);
|
|
71
|
+
}
|
|
72
|
+
console.log(`🔌 Provider detected: ${this.detectedProvider}\n`);
|
|
50
73
|
// Step 2: Initialize
|
|
51
74
|
if (this.config.memory?.enabled) {
|
|
52
75
|
this.memoryManager = new MemoryManager(this.config.memory, this.config.ai.provider, this.config.ai.model);
|
|
@@ -61,12 +84,26 @@ export class YamaOrchestrator {
|
|
|
61
84
|
this.initialized = true;
|
|
62
85
|
}
|
|
63
86
|
// Step 4: Mode-specific setup
|
|
64
|
-
if (mode === "pr"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
if (mode === "pr") {
|
|
88
|
+
// If MCP was already set up for a DIFFERENT provider (one instance
|
|
89
|
+
// reviewing a Bitbucket PR then a GitHub PR), tear down the previous
|
|
90
|
+
// provider's server and re-register for the new one. Single-provider
|
|
91
|
+
// runs never hit this branch, so their behaviour is unchanged.
|
|
92
|
+
if (this.mcpInitialized &&
|
|
93
|
+
this.mcpProvider !== null &&
|
|
94
|
+
this.mcpProvider !== this.detectedProvider) {
|
|
95
|
+
console.log(`🔄 Provider changed (${this.mcpProvider} → ${this.detectedProvider}); re-registering MCP servers...`);
|
|
96
|
+
await this.mcpManager.resetForProviderSwitch(this.neurolink, this.mcpProvider);
|
|
97
|
+
this.mcpInitialized = false;
|
|
98
|
+
}
|
|
99
|
+
if (!this.mcpInitialized) {
|
|
100
|
+
await this.mcpManager.setupMCPServers(this.neurolink, this.config.mcpServers, this.detectedProvider);
|
|
101
|
+
if (this.explorer && this.config.ai.explore.enabled) {
|
|
102
|
+
await this.explorer.initialize("pr");
|
|
103
|
+
}
|
|
104
|
+
this.mcpInitialized = true;
|
|
105
|
+
this.mcpProvider = this.detectedProvider;
|
|
68
106
|
}
|
|
69
|
-
this.mcpInitialized = true;
|
|
70
107
|
}
|
|
71
108
|
else if (mode === "local" && !this.localGitMcpInitialized) {
|
|
72
109
|
await this.mcpManager.setupLocalGitMCPServer(this.neurolink);
|
|
@@ -75,8 +112,9 @@ export class YamaOrchestrator {
|
|
|
75
112
|
}
|
|
76
113
|
this.localGitMcpInitialized = true;
|
|
77
114
|
}
|
|
78
|
-
// Step 5: Mode-specific validation
|
|
79
|
-
|
|
115
|
+
// Step 5: Mode-specific validation (provider-aware: GitHub runs skip the
|
|
116
|
+
// Bitbucket env requirement, Bitbucket runs are unchanged)
|
|
117
|
+
await this.configLoader.validate(mode, this.detectedProvider);
|
|
80
118
|
console.log("✅ Yama initialized successfully\n");
|
|
81
119
|
console.log("═".repeat(60) + "\n");
|
|
82
120
|
}
|
|
@@ -89,14 +127,16 @@ export class YamaOrchestrator {
|
|
|
89
127
|
* Start autonomous AI review
|
|
90
128
|
*/
|
|
91
129
|
async startReview(request) {
|
|
92
|
-
await this.ensureInitialized("pr", request.configPath);
|
|
130
|
+
await this.ensureInitialized("pr", request.configPath, request);
|
|
131
|
+
// Detect the provider based on request and environment
|
|
132
|
+
this.detectedProvider = ProviderDetector.detect(request, process.env, this.config.defaultProvider);
|
|
93
133
|
const startTime = Date.now();
|
|
94
134
|
const sessionId = this.sessionManager.createSession(request);
|
|
95
135
|
this.logReviewStart(request, sessionId);
|
|
96
136
|
try {
|
|
97
137
|
const bootstrapStandards = await this.getBootstrappedStandards(request, sessionId);
|
|
98
138
|
// Build comprehensive AI instructions
|
|
99
|
-
const instructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards);
|
|
139
|
+
const instructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards, this.detectedProvider);
|
|
100
140
|
if (this.config.display.verboseToolCalls) {
|
|
101
141
|
console.log("\n📝 AI Instructions built:");
|
|
102
142
|
console.log(` Instruction length: ${instructions.length} characters\n`);
|
|
@@ -218,12 +258,12 @@ export class YamaOrchestrator {
|
|
|
218
258
|
* Stream review with real-time updates (for verbose mode)
|
|
219
259
|
*/
|
|
220
260
|
async *streamReview(request) {
|
|
221
|
-
await this.ensureInitialized("pr", request.configPath);
|
|
261
|
+
await this.ensureInitialized("pr", request.configPath, request);
|
|
222
262
|
const sessionId = this.sessionManager.createSession(request);
|
|
223
263
|
try {
|
|
224
264
|
const bootstrapStandards = await this.getBootstrappedStandards(request, sessionId);
|
|
225
265
|
// Build instructions
|
|
226
|
-
const instructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards);
|
|
266
|
+
const instructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards, this.detectedProvider);
|
|
227
267
|
// Create tool context
|
|
228
268
|
const toolContext = this.createToolContext(sessionId, request);
|
|
229
269
|
this.setToolContext(toolContext);
|
|
@@ -274,7 +314,7 @@ export class YamaOrchestrator {
|
|
|
274
314
|
* This allows the AI to use knowledge gained during review to write better descriptions
|
|
275
315
|
*/
|
|
276
316
|
async startReviewAndEnhance(request) {
|
|
277
|
-
await this.ensureInitialized("pr", request.configPath);
|
|
317
|
+
await this.ensureInitialized("pr", request.configPath, request);
|
|
278
318
|
const startTime = Date.now();
|
|
279
319
|
const sessionId = this.sessionManager.createSession(request);
|
|
280
320
|
this.logReviewStart(request, sessionId);
|
|
@@ -284,7 +324,7 @@ export class YamaOrchestrator {
|
|
|
284
324
|
// ========================================================================
|
|
285
325
|
const bootstrapStandards = await this.getBootstrappedStandards(request, sessionId);
|
|
286
326
|
// Build review instructions
|
|
287
|
-
const reviewInstructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards);
|
|
327
|
+
const reviewInstructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards, this.detectedProvider);
|
|
288
328
|
if (this.config.display.verboseToolCalls) {
|
|
289
329
|
console.log("\n📝 Review instructions built:");
|
|
290
330
|
console.log(` Instruction length: ${reviewInstructions.length} characters\n`);
|
|
@@ -331,7 +371,7 @@ export class YamaOrchestrator {
|
|
|
331
371
|
if (this.config.descriptionEnhancement.enabled) {
|
|
332
372
|
console.log("📝 Phase 2: Enhancing PR description...");
|
|
333
373
|
console.log(" AI will use review insights to write description\n");
|
|
334
|
-
const enhanceInstructions = await this.promptBuilder.buildDescriptionEnhancementInstructions(request, this.config);
|
|
374
|
+
const enhanceInstructions = await this.promptBuilder.buildDescriptionEnhancementInstructions(request, this.config, this.detectedProvider);
|
|
335
375
|
// Continue the SAME session - AI remembers everything from review
|
|
336
376
|
const enhanceResponse = await this.neurolink.generate({
|
|
337
377
|
input: { text: enhanceInstructions },
|
|
@@ -376,11 +416,11 @@ export class YamaOrchestrator {
|
|
|
376
416
|
* Enhance PR description only (without full review)
|
|
377
417
|
*/
|
|
378
418
|
async enhanceDescription(request) {
|
|
379
|
-
await this.ensureInitialized("pr", request.configPath);
|
|
419
|
+
await this.ensureInitialized("pr", request.configPath, request);
|
|
380
420
|
const sessionId = this.sessionManager.createSession(request);
|
|
381
421
|
try {
|
|
382
422
|
console.log("\n📝 Enhancing PR description...\n");
|
|
383
|
-
const instructions = await this.promptBuilder.buildDescriptionEnhancementInstructions(request, this.config);
|
|
423
|
+
const instructions = await this.promptBuilder.buildDescriptionEnhancementInstructions(request, this.config, this.detectedProvider);
|
|
384
424
|
const toolContext = this.createToolContext(sessionId, request);
|
|
385
425
|
this.setToolContext(toolContext);
|
|
386
426
|
const aiResponse = await this.neurolink.generate({
|
|
@@ -429,15 +469,28 @@ export class YamaOrchestrator {
|
|
|
429
469
|
return this.sessionManager.exportSession(sessionId);
|
|
430
470
|
}
|
|
431
471
|
getUserId(request) {
|
|
432
|
-
|
|
472
|
+
// Coalesce provider-specific identifiers: Bitbucket uses workspace/repository,
|
|
473
|
+
// GitHub uses owner/repo. Bitbucket behavior is unchanged when those are set.
|
|
474
|
+
const workspace = request.workspace || request.owner || "";
|
|
475
|
+
const repository = request.repository || request.repo || "";
|
|
476
|
+
return `${workspace}-${repository}`.toLowerCase();
|
|
433
477
|
}
|
|
434
478
|
createToolContext(sessionId, request) {
|
|
435
479
|
return {
|
|
436
480
|
sessionId,
|
|
437
481
|
mode: "pr",
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
482
|
+
// Carry the detected provider so explore_context / tool runtime context
|
|
483
|
+
// knows which provider's toolset to use (GitHub reviews otherwise saw
|
|
484
|
+
// provider=undefined and defaulted to Bitbucket).
|
|
485
|
+
provider: this.detectedProvider,
|
|
486
|
+
// Coalesce provider-specific identifiers (GitHub owner/repo, Bitbucket
|
|
487
|
+
// workspace/repository) into the existing context fields. No-op for
|
|
488
|
+
// Bitbucket where workspace/repository are already set.
|
|
489
|
+
workspace: request.workspace || request.owner,
|
|
490
|
+
repository: request.repository || request.repo,
|
|
491
|
+
// GitHub passes the PR number via prNumber; coalesce so the runtime
|
|
492
|
+
// context has a PR number for both providers.
|
|
493
|
+
pullRequestId: request.pullRequestId ?? request.prNumber,
|
|
441
494
|
branch: request.branch,
|
|
442
495
|
dryRun: request.dryRun || false,
|
|
443
496
|
metadata: {
|
|
@@ -450,8 +503,12 @@ export class YamaOrchestrator {
|
|
|
450
503
|
return {
|
|
451
504
|
sessionId,
|
|
452
505
|
mode: "local",
|
|
453
|
-
|
|
454
|
-
|
|
506
|
+
// Carry the detected provider for explore_context toolset selection.
|
|
507
|
+
provider: this.detectedProvider,
|
|
508
|
+
// Coalesce provider-specific identifiers (GitHub owner/repo, Bitbucket
|
|
509
|
+
// workspace/repository). No-op for Bitbucket where they are already set.
|
|
510
|
+
workspace: request.workspace || request.owner,
|
|
511
|
+
repository: request.repository || request.repo,
|
|
455
512
|
dryRun: request.dryRun || false,
|
|
456
513
|
metadata: {
|
|
457
514
|
yamaVersion: "2.2.1",
|
|
@@ -479,7 +536,9 @@ export class YamaOrchestrator {
|
|
|
479
536
|
const statistics = this.calculateStatistics(session);
|
|
480
537
|
return {
|
|
481
538
|
mode: "pr",
|
|
482
|
-
|
|
539
|
+
// GitHub passes the PR number via prNumber; coalesce so GitHub reviews
|
|
540
|
+
// report the real PR number instead of 0.
|
|
541
|
+
prId: session.request.pullRequestId ?? session.request.prNumber ?? 0,
|
|
483
542
|
decision,
|
|
484
543
|
statistics,
|
|
485
544
|
summary: this.extractSummary(aiResponse),
|
|
@@ -756,36 +815,25 @@ export class YamaOrchestrator {
|
|
|
756
815
|
* Extract decision from AI response
|
|
757
816
|
*/
|
|
758
817
|
extractDecision(aiResponse, session) {
|
|
759
|
-
// Derive final review state from tool calls
|
|
760
|
-
//
|
|
818
|
+
// Derive final review state from tool calls, delegating provider-specific
|
|
819
|
+
// signal interpretation to the detected provider's toolset. The Bitbucket
|
|
820
|
+
// toolset replicates the previous hardcoded logic (set_review_status /
|
|
821
|
+
// set_pr_approval + legacy names) byte-for-byte.
|
|
761
822
|
const toolCalls = session.toolCalls || [];
|
|
823
|
+
const ts = getProviderToolset(this.detectedProvider);
|
|
762
824
|
let requestedChanges;
|
|
763
825
|
let approved;
|
|
764
826
|
for (const tc of toolCalls) {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
else if (name === "request_changes") {
|
|
827
|
+
const decision = ts.interpretDecision({
|
|
828
|
+
toolName: tc?.toolName,
|
|
829
|
+
args: tc?.args || {},
|
|
830
|
+
});
|
|
831
|
+
if (decision === "BLOCKED") {
|
|
773
832
|
requestedChanges = true;
|
|
774
833
|
}
|
|
775
|
-
else if (
|
|
776
|
-
requestedChanges = false;
|
|
777
|
-
}
|
|
778
|
-
else if (name === "set_pr_approval") {
|
|
779
|
-
if (typeof args.approved === "boolean") {
|
|
780
|
-
approved = args.approved;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
else if (name === "approve_pull_request") {
|
|
834
|
+
else if (decision === "APPROVED") {
|
|
784
835
|
approved = true;
|
|
785
836
|
}
|
|
786
|
-
else if (name === "unapprove_pull_request") {
|
|
787
|
-
approved = false;
|
|
788
|
-
}
|
|
789
837
|
}
|
|
790
838
|
if (requestedChanges === true) {
|
|
791
839
|
return "BLOCKED";
|
|
@@ -801,10 +849,12 @@ export class YamaOrchestrator {
|
|
|
801
849
|
*/
|
|
802
850
|
calculateStatistics(session) {
|
|
803
851
|
const toolCalls = session.toolCalls || [];
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
//
|
|
807
|
-
const
|
|
852
|
+
const ts = getProviderToolset(this.detectedProvider);
|
|
853
|
+
// Count the changed files actually reviewed. Provider-specific because the
|
|
854
|
+
// diff-tool semantics differ.
|
|
855
|
+
const filesReviewed = this.countFilesReviewed(toolCalls);
|
|
856
|
+
// Try to extract issue counts from comments (provider-specific comment tools)
|
|
857
|
+
const commentCalls = toolCalls.filter((tc) => ts.commentToolNames.includes(tc.toolName));
|
|
808
858
|
const issuesFound = this.extractIssueCountsFromComments(commentCalls);
|
|
809
859
|
return {
|
|
810
860
|
filesReviewed,
|
|
@@ -816,6 +866,54 @@ export class YamaOrchestrator {
|
|
|
816
866
|
totalComments: commentCalls.length,
|
|
817
867
|
};
|
|
818
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Count the changed files actually reviewed, from the session tool calls.
|
|
871
|
+
*
|
|
872
|
+
* Bitbucket: get_pull_request_diff is called once per file by design, so the
|
|
873
|
+
* raw count of diff-tool calls already equals the files reviewed. This path is
|
|
874
|
+
* byte-for-byte identical to the previous behaviour.
|
|
875
|
+
*
|
|
876
|
+
* GitHub: pull_request_read is overloaded — the SAME tool name serves
|
|
877
|
+
* method="get" (PR shell), "get_files" (changed-file list) and "get_diff"
|
|
878
|
+
* (unified diff). Counting every pull_request_read call therefore inflates the
|
|
879
|
+
* number. Instead, count DISTINCT file paths the review actually touched, taken
|
|
880
|
+
* from the inline pending-review comments (add_comment_to_pending_review →
|
|
881
|
+
* args.path). Fall back to the number of distinct get_files/get_diff reads when
|
|
882
|
+
* no inline comments were posted (e.g. a clean approval), so a reviewed-but-
|
|
883
|
+
* clean PR is not reported as 0 files.
|
|
884
|
+
*/
|
|
885
|
+
countFilesReviewed(toolCalls) {
|
|
886
|
+
const ts = getProviderToolset(this.detectedProvider);
|
|
887
|
+
if (this.detectedProvider !== "github") {
|
|
888
|
+
// Bitbucket (and any non-GitHub provider): one diff fetch per file.
|
|
889
|
+
return toolCalls.filter((tc) => ts.diffToolNames.includes(tc.toolName)).length;
|
|
890
|
+
}
|
|
891
|
+
// GitHub: prefer distinct paths from inline comments.
|
|
892
|
+
const commentedPaths = new Set();
|
|
893
|
+
for (const tc of toolCalls) {
|
|
894
|
+
if (tc?.toolName !== "add_comment_to_pending_review") {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const path = tc?.args?.path;
|
|
898
|
+
if (typeof path === "string" && path.length > 0) {
|
|
899
|
+
commentedPaths.add(path);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (commentedPaths.size > 0) {
|
|
903
|
+
return commentedPaths.size;
|
|
904
|
+
}
|
|
905
|
+
// Fallback for clean PRs with no inline comments: count the distinct
|
|
906
|
+
// changed-file / diff reads (pull_request_read with method get_files/get_diff)
|
|
907
|
+
// rather than every pull_request_read (which includes the single "get").
|
|
908
|
+
const diffReads = toolCalls.filter((tc) => {
|
|
909
|
+
if (!ts.diffToolNames.includes(tc?.toolName)) {
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
const method = tc?.args?.method;
|
|
913
|
+
return method === "get_files" || method === "get_diff";
|
|
914
|
+
}).length;
|
|
915
|
+
return diffReads;
|
|
916
|
+
}
|
|
819
917
|
/**
|
|
820
918
|
* Extract issue counts from comment tool calls
|
|
821
919
|
*/
|
|
@@ -1020,6 +1118,9 @@ export class YamaOrchestrator {
|
|
|
1020
1118
|
mode: runtimeMode,
|
|
1021
1119
|
workspace: String(mergedContext.workspace || "local"),
|
|
1022
1120
|
repository: String(mergedContext.repository || "repository"),
|
|
1121
|
+
provider: typeof mergedContext.provider === "string"
|
|
1122
|
+
? mergedContext.provider
|
|
1123
|
+
: undefined,
|
|
1023
1124
|
pullRequestId: typeof mergedContext.pullRequestId === "number"
|
|
1024
1125
|
? mergedContext.pullRequestId
|
|
1025
1126
|
: undefined,
|
|
@@ -1063,20 +1164,29 @@ export class YamaOrchestrator {
|
|
|
1063
1164
|
if (!this.config.ai.explore.enabled || !this.explorer) {
|
|
1064
1165
|
return "";
|
|
1065
1166
|
}
|
|
1066
|
-
|
|
1167
|
+
// Validate required parameters for bootstrap. Coalesce provider-specific
|
|
1168
|
+
// identifiers so GitHub (owner/repo) also gets bootstrap standards; no-op
|
|
1169
|
+
// for Bitbucket where workspace/repository are already set.
|
|
1170
|
+
const workspace = (request.workspace || request.owner || "").trim();
|
|
1171
|
+
const repository = (request.repository || request.repo || "").trim();
|
|
1172
|
+
if (workspace.length === 0 || repository.length === 0) {
|
|
1173
|
+
return "";
|
|
1174
|
+
}
|
|
1175
|
+
const cacheKey = `${workspace}/${repository}`.toLowerCase();
|
|
1067
1176
|
const cached = this.bootstrapStandardsCache.get(cacheKey);
|
|
1068
1177
|
if (cached !== undefined) {
|
|
1069
1178
|
return cached;
|
|
1070
1179
|
}
|
|
1071
|
-
const task = `Bootstrap repo review standards for ${
|
|
1180
|
+
const task = `Bootstrap repo review standards for ${workspace}/${repository}. Inspect the last 15 merged pull requests on this repository. For each, collect inline and summary comments left by HUMAN reviewers (ignore bot authors like "Yama", "yama-bot", "yama-review", "euler.bot"). Focus on comments that asked for code changes, flagged bugs, pushed back on an approach, or cited a convention. Distill the recurring patterns and anti-patterns the human reviewers care about — especially things that a static config rule set would miss (naming, error-handling idioms, module boundaries, test expectations, migration rules, review etiquette). Return a concise bullet list of 10-20 patterns. Each bullet: one sentence stating the pattern, optionally one sentence of rationale. Do NOT include PR numbers, author names, or raw quotes — just distilled patterns.`;
|
|
1072
1181
|
try {
|
|
1073
1182
|
console.log(` 🧭 Bootstrapping repo standards from recent PRs (one-time per process)...`);
|
|
1074
|
-
const { result } = await this.explorer.explore({ task, focus: [
|
|
1183
|
+
const { result } = await this.explorer.explore({ task, focus: [repository] }, {
|
|
1075
1184
|
sessionId,
|
|
1076
1185
|
mode: "pr",
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1186
|
+
provider: this.detectedProvider,
|
|
1187
|
+
workspace,
|
|
1188
|
+
repository,
|
|
1189
|
+
pullRequestId: request.pullRequestId ?? request.prNumber,
|
|
1080
1190
|
branch: request.branch,
|
|
1081
1191
|
dryRun: request.dryRun || false,
|
|
1082
1192
|
metadata: { bootstrap: true },
|
|
@@ -1115,8 +1225,8 @@ export class YamaOrchestrator {
|
|
|
1115
1225
|
/**
|
|
1116
1226
|
* Ensure orchestrator is initialized
|
|
1117
1227
|
*/
|
|
1118
|
-
async ensureInitialized(mode = "pr", configPath) {
|
|
1119
|
-
await this.initialize(configPath, mode);
|
|
1228
|
+
async ensureInitialized(mode = "pr", configPath, request) {
|
|
1229
|
+
await this.initialize(configPath, mode, request);
|
|
1120
1230
|
}
|
|
1121
1231
|
/**
|
|
1122
1232
|
* Show Yama banner
|
|
@@ -18,7 +18,16 @@ export declare class ContextExplorerService {
|
|
|
18
18
|
explore(input: ExploreContextInput, runtimeContext: ExploreRuntimeContext): Promise<ExploreExecutionResult>;
|
|
19
19
|
private normalizeInput;
|
|
20
20
|
private buildCacheKey;
|
|
21
|
+
private getUserId;
|
|
21
22
|
private getToolFilteringOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Provider-agnostic mutation block list for the read-only explore subagent.
|
|
25
|
+
* Derived once from each provider toolset's mutationToolNames (Bitbucket +
|
|
26
|
+
* GitHub) so the source of truth stays in ProviderToolset, not re-hardcoded
|
|
27
|
+
* here. Names are lower-cased for case-insensitive matching against the
|
|
28
|
+
* normalized tool name.
|
|
29
|
+
*/
|
|
30
|
+
private static readonly MUTATION_TOOL_NAMES;
|
|
22
31
|
private shouldExcludeTool;
|
|
23
32
|
private normalizeToolName;
|
|
24
33
|
private normalizeResult;
|
|
@@ -5,6 +5,7 @@ import { NeuroLink } from "@juspay/neurolink";
|
|
|
5
5
|
import { MCPServerManager } from "../core/MCPServerManager.js";
|
|
6
6
|
import { KnowledgeBaseManager } from "../learning/KnowledgeBaseManager.js";
|
|
7
7
|
import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
|
|
8
|
+
import { getProviderToolset } from "../providers/ProviderToolset.js";
|
|
8
9
|
import { ExplorerPromptBuilder } from "./ExplorerPromptBuilder.js";
|
|
9
10
|
import { RulesContextLoader } from "./RulesContextLoader.js";
|
|
10
11
|
export class ContextExplorerService {
|
|
@@ -72,7 +73,7 @@ export class ContextExplorerService {
|
|
|
72
73
|
...this.getToolFilteringOptions(runtimeContext.mode),
|
|
73
74
|
context: {
|
|
74
75
|
sessionId: runtimeContext.sessionId,
|
|
75
|
-
userId:
|
|
76
|
+
userId: this.getUserId(runtimeContext),
|
|
76
77
|
operation: "explore-context-research",
|
|
77
78
|
metadata: runtimeContext.metadata || {},
|
|
78
79
|
},
|
|
@@ -91,7 +92,7 @@ export class ContextExplorerService {
|
|
|
91
92
|
disableTools: true,
|
|
92
93
|
context: {
|
|
93
94
|
sessionId: runtimeContext.sessionId,
|
|
94
|
-
userId:
|
|
95
|
+
userId: this.getUserId(runtimeContext),
|
|
95
96
|
operation: "explore-context-extraction",
|
|
96
97
|
metadata: runtimeContext.metadata || {},
|
|
97
98
|
},
|
|
@@ -122,16 +123,48 @@ export class ContextExplorerService {
|
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
buildCacheKey(input, runtimeContext) {
|
|
126
|
+
// Validate required string parameters
|
|
127
|
+
const task = typeof input.task === "string" ? input.task.trim().toLowerCase() : "";
|
|
128
|
+
const workspace = typeof runtimeContext.workspace === "string"
|
|
129
|
+
? runtimeContext.workspace.trim()
|
|
130
|
+
: "";
|
|
131
|
+
const repository = typeof runtimeContext.repository === "string"
|
|
132
|
+
? runtimeContext.repository.trim()
|
|
133
|
+
: "";
|
|
134
|
+
if (!task) {
|
|
135
|
+
throw new Error("Invalid cache key: task must be a non-empty string");
|
|
136
|
+
}
|
|
137
|
+
if (!workspace || !repository) {
|
|
138
|
+
throw new Error("Invalid cache key: workspace and repository must be non-empty strings");
|
|
139
|
+
}
|
|
125
140
|
return JSON.stringify({
|
|
126
141
|
mode: runtimeContext.mode,
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
provider: typeof runtimeContext.provider === "string"
|
|
143
|
+
? runtimeContext.provider.trim().toLowerCase()
|
|
144
|
+
: "bitbucket",
|
|
145
|
+
workspace,
|
|
146
|
+
repository,
|
|
129
147
|
pullRequestId: runtimeContext.pullRequestId || null,
|
|
130
|
-
branch: runtimeContext.branch
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
branch: typeof runtimeContext.branch === "string"
|
|
149
|
+
? runtimeContext.branch.trim()
|
|
150
|
+
: null,
|
|
151
|
+
task,
|
|
152
|
+
focus: Array.isArray(input.focus)
|
|
153
|
+
? input.focus
|
|
154
|
+
.filter((item) => typeof item === "string")
|
|
155
|
+
.map((item) => item.trim().toLowerCase())
|
|
156
|
+
: [],
|
|
133
157
|
});
|
|
134
158
|
}
|
|
159
|
+
getUserId(runtimeContext) {
|
|
160
|
+
const workspace = typeof runtimeContext.workspace === "string"
|
|
161
|
+
? runtimeContext.workspace.trim()
|
|
162
|
+
: "unknown";
|
|
163
|
+
const repository = typeof runtimeContext.repository === "string"
|
|
164
|
+
? runtimeContext.repository.trim()
|
|
165
|
+
: "unknown";
|
|
166
|
+
return `${workspace}-${repository}`.toLowerCase();
|
|
167
|
+
}
|
|
135
168
|
getToolFilteringOptions(mode) {
|
|
136
169
|
try {
|
|
137
170
|
const externalTools = this.neurolink.getExternalMCPTools?.();
|
|
@@ -148,9 +181,17 @@ export class ContextExplorerService {
|
|
|
148
181
|
return {};
|
|
149
182
|
}
|
|
150
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Provider-agnostic mutation block list for the read-only explore subagent.
|
|
186
|
+
* Derived once from each provider toolset's mutationToolNames (Bitbucket +
|
|
187
|
+
* GitHub) so the source of truth stays in ProviderToolset, not re-hardcoded
|
|
188
|
+
* here. Names are lower-cased for case-insensitive matching against the
|
|
189
|
+
* normalized tool name.
|
|
190
|
+
*/
|
|
191
|
+
static MUTATION_TOOL_NAMES = new Set(["bitbucket", "github"].flatMap((provider) => getProviderToolset(provider).mutationToolNames.map((name) => name.toLowerCase())));
|
|
151
192
|
shouldExcludeTool(mode, toolName) {
|
|
152
193
|
const normalized = this.normalizeToolName(toolName);
|
|
153
|
-
if (
|
|
194
|
+
if (ContextExplorerService.MUTATION_TOOL_NAMES.has(normalized.toLowerCase())) {
|
|
154
195
|
return true;
|
|
155
196
|
}
|
|
156
197
|
if (mode === "local") {
|
|
@@ -430,7 +471,7 @@ Rules:
|
|
|
430
471
|
if (!this.memoryManager) {
|
|
431
472
|
return null;
|
|
432
473
|
}
|
|
433
|
-
return this.memoryManager.readRepositoryMemory(runtimeContext.workspace, runtimeContext.repository);
|
|
474
|
+
return this.memoryManager.readRepositoryMemory(runtimeContext.workspace, runtimeContext.repository, runtimeContext.provider || "bitbucket");
|
|
434
475
|
}
|
|
435
476
|
initializeNeurolink() {
|
|
436
477
|
const observabilityConfig = buildObservabilityConfigFromEnv();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Learning Types
|
|
3
3
|
* Type definitions for the knowledge base and learning extraction system
|
|
4
4
|
*/
|
|
5
|
+
import type { VCSProviderName } from "../providers/ProviderToolset.js";
|
|
5
6
|
/**
|
|
6
7
|
* Categories for extracted learnings
|
|
7
8
|
* Maps to sections in the knowledge base file
|
|
@@ -69,6 +70,7 @@ export interface LearnRequest {
|
|
|
69
70
|
workspace: string;
|
|
70
71
|
repository: string;
|
|
71
72
|
pullRequestId: number;
|
|
73
|
+
provider?: VCSProviderName;
|
|
72
74
|
dryRun?: boolean;
|
|
73
75
|
/** @deprecated Use commitMode instead */
|
|
74
76
|
commit?: boolean;
|
|
@@ -36,18 +36,34 @@ export declare class MemoryManager {
|
|
|
36
36
|
*/
|
|
37
37
|
buildNeuroLinkMemoryConfig(): Record<string, unknown>;
|
|
38
38
|
/**
|
|
39
|
-
* Build a deterministic owner ID from workspace and repository.
|
|
39
|
+
* Build a deterministic owner ID from provider, workspace, and repository.
|
|
40
|
+
* Isolates memory per platform to prevent collisions between same repo names
|
|
41
|
+
* on different platforms (e.g., "repo" on GitHub vs Bitbucket).
|
|
42
|
+
*
|
|
43
|
+
* Format:
|
|
44
|
+
* - GitHub: github-{owner}-{repo}
|
|
45
|
+
* - Bitbucket: bitbucket-{workspace}-{repo}
|
|
46
|
+
*
|
|
40
47
|
* This value is passed as `context.userId` in generate() calls.
|
|
41
48
|
*/
|
|
42
|
-
static buildOwnerId(workspace: string, repository: string): string;
|
|
49
|
+
static buildOwnerId(workspace: string, repository: string, provider?: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Build the legacy (unprefixed) owner ID used before provider isolation.
|
|
52
|
+
*
|
|
53
|
+
* Format: {workspace}-{repository}
|
|
54
|
+
*
|
|
55
|
+
* Used as a read-time fallback so memory written before the provider-prefix
|
|
56
|
+
* upgrade keeps working. New writes always use the provider-prefixed key.
|
|
57
|
+
*/
|
|
58
|
+
static buildLegacyOwnerId(workspace: string, repository: string): string;
|
|
43
59
|
/**
|
|
44
60
|
* Read persisted condensed memory for a repository owner ID.
|
|
45
61
|
*/
|
|
46
62
|
readMemory(ownerId: string): Promise<string | null>;
|
|
47
63
|
/**
|
|
48
|
-
* Read persisted condensed memory for a workspace/repository pair.
|
|
64
|
+
* Read persisted condensed memory for a workspace/repository pair on a specific provider.
|
|
49
65
|
*/
|
|
50
|
-
readRepositoryMemory(workspace: string, repository: string): Promise<string | null>;
|
|
66
|
+
readRepositoryMemory(workspace: string, repository: string, provider?: string): Promise<string | null>;
|
|
51
67
|
/**
|
|
52
68
|
* Commit memory files to the repository if autoCommit is enabled.
|
|
53
69
|
*
|
|
@@ -123,10 +123,29 @@ export class MemoryManager {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* Build a deterministic owner ID from workspace and repository.
|
|
126
|
+
* Build a deterministic owner ID from provider, workspace, and repository.
|
|
127
|
+
* Isolates memory per platform to prevent collisions between same repo names
|
|
128
|
+
* on different platforms (e.g., "repo" on GitHub vs Bitbucket).
|
|
129
|
+
*
|
|
130
|
+
* Format:
|
|
131
|
+
* - GitHub: github-{owner}-{repo}
|
|
132
|
+
* - Bitbucket: bitbucket-{workspace}-{repo}
|
|
133
|
+
*
|
|
127
134
|
* This value is passed as `context.userId` in generate() calls.
|
|
128
135
|
*/
|
|
129
|
-
static buildOwnerId(workspace, repository) {
|
|
136
|
+
static buildOwnerId(workspace, repository, provider = "bitbucket") {
|
|
137
|
+
const providerPrefix = provider.toLowerCase();
|
|
138
|
+
return `${providerPrefix}-${workspace}-${repository}`.toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the legacy (unprefixed) owner ID used before provider isolation.
|
|
142
|
+
*
|
|
143
|
+
* Format: {workspace}-{repository}
|
|
144
|
+
*
|
|
145
|
+
* Used as a read-time fallback so memory written before the provider-prefix
|
|
146
|
+
* upgrade keeps working. New writes always use the provider-prefixed key.
|
|
147
|
+
*/
|
|
148
|
+
static buildLegacyOwnerId(workspace, repository) {
|
|
130
149
|
return `${workspace}-${repository}`.toLowerCase();
|
|
131
150
|
}
|
|
132
151
|
/**
|
|
@@ -146,10 +165,16 @@ export class MemoryManager {
|
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
/**
|
|
149
|
-
* Read persisted condensed memory for a workspace/repository pair.
|
|
168
|
+
* Read persisted condensed memory for a workspace/repository pair on a specific provider.
|
|
150
169
|
*/
|
|
151
|
-
async readRepositoryMemory(workspace, repository) {
|
|
152
|
-
|
|
170
|
+
async readRepositoryMemory(workspace, repository, provider = "bitbucket") {
|
|
171
|
+
const prefixed = await this.readMemory(MemoryManager.buildOwnerId(workspace, repository, provider));
|
|
172
|
+
if (prefixed !== null) {
|
|
173
|
+
return prefixed;
|
|
174
|
+
}
|
|
175
|
+
// Fall back to the legacy unprefixed key so memory written before the
|
|
176
|
+
// provider-prefix upgrade is not silently dropped after upgrading.
|
|
177
|
+
return this.readMemory(MemoryManager.buildLegacyOwnerId(workspace, repository));
|
|
153
178
|
}
|
|
154
179
|
/**
|
|
155
180
|
* Commit memory files to the repository if autoCommit is enabled.
|