@juspay/yama 2.4.2 → 2.6.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [2.6.0](https://github.com/juspay/yama/compare/v2.5.0...v2.6.0) (2026-06-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * **action:** add Yama self-review workflow with LiteLLM/Vertex auth ([5ebb73f](https://github.com/juspay/yama/commit/5ebb73f2450951b5f97d97a6a3caa89377a150af))
7
+
8
+ # [2.5.0](https://github.com/juspay/yama/compare/v2.4.2...v2.5.0) (2026-06-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * **github:** add GitHub provider for PR reviews ([0b8e45f](https://github.com/juspay/yama/commit/0b8e45f30a95a30988c0dc6f84e929d2338108fe))
14
+
1
15
  ## [2.4.2](https://github.com/juspay/yama/compare/v2.4.1...v2.4.2) (2026-04-24)
2
16
 
3
17
 
package/dist/cli/cli.js CHANGED
@@ -43,6 +43,8 @@ function setupReviewCommand() {
43
43
  .option("--mode <mode>", "Review mode (pr|local)", "pr")
44
44
  .option("-w, --workspace <workspace>", "Bitbucket workspace")
45
45
  .option("-r, --repository <repository>", "Repository name")
46
+ .option("--owner <owner>", "GitHub owner/organization")
47
+ .option("--repo <repo>", "GitHub repository (alternative to --repository)")
46
48
  .option("-p, --pr <id>", "Pull request ID")
47
49
  .option("-b, --branch <branch>", "Branch name (finds PR automatically)")
48
50
  .option("--repo-path <path>", "Local repository path (local mode)")
@@ -93,10 +95,39 @@ function setupReviewCommand() {
93
95
  }
94
96
  process.exit(result.decision === "BLOCKED" ? 1 : 0);
95
97
  }
96
- if (!options.workspace || !options.repository) {
97
- console.error("❌ Error: --workspace and --repository are required in pr mode");
98
+ // Auto-detect provider and validate required parameters.
99
+ // --repo is GitHub-only; Bitbucket uses --repository so that
100
+ // 'yama review --workspace acme --repo service' is not misread as mixed.
101
+ const hasGitHub = options.owner || options.repo;
102
+ const hasBitbucket = options.workspace || options.repository;
103
+ if (hasGitHub && hasBitbucket) {
104
+ console.error("❌ Error: Cannot mix GitHub (--owner/--repo) and Bitbucket (--workspace/--repository) parameters");
98
105
  process.exit(1);
99
106
  }
107
+ if (!hasGitHub && !hasBitbucket) {
108
+ console.error("❌ Error: Either GitHub (--owner and --repo) or Bitbucket (--workspace and --repository) parameters are required");
109
+ process.exit(1);
110
+ }
111
+ if (hasGitHub) {
112
+ if (!options.owner) {
113
+ console.error("❌ Error: --owner is required when using GitHub parameters");
114
+ process.exit(1);
115
+ }
116
+ if (!options.repo) {
117
+ console.error("❌ Error: --repo is required when using GitHub parameters");
118
+ process.exit(1);
119
+ }
120
+ }
121
+ else {
122
+ if (!options.workspace) {
123
+ console.error("❌ Error: --workspace is required when using Bitbucket parameters");
124
+ process.exit(1);
125
+ }
126
+ if (!options.repository) {
127
+ console.error("❌ Error: --repository is required when using Bitbucket parameters");
128
+ process.exit(1);
129
+ }
130
+ }
100
131
  if (!options.pr && !options.branch) {
101
132
  console.error("❌ Error: Either --pr or --branch must be specified");
102
133
  process.exit(1);
@@ -111,9 +142,17 @@ function setupReviewCommand() {
111
142
  }
112
143
  const request = {
113
144
  mode: "pr",
145
+ provider: hasGitHub ? "github" : "bitbucket",
146
+ owner: options.owner,
147
+ repo: options.repo,
114
148
  workspace: options.workspace,
115
149
  repository: options.repository,
116
150
  pullRequestId,
151
+ // GitHub SDK/prompt paths read prNumber; mirror pullRequestId for
152
+ // GitHub only so Bitbucket behavior stays unchanged.
153
+ prNumber: hasGitHub && typeof pullRequestId === "number"
154
+ ? pullRequestId
155
+ : undefined,
117
156
  branch: options.branch,
118
157
  dryRun: globalOpts.dryRun || false,
119
158
  verbose: globalOpts.verbose || false,
@@ -122,7 +161,9 @@ function setupReviewCommand() {
122
161
  focus,
123
162
  outputSchemaVersion: options.outputSchemaVersion,
124
163
  };
125
- await yama.initialize(request.configPath);
164
+ // Thread the request into initialize so MCP servers are set up for the
165
+ // correct provider (GitHub vs Bitbucket) before review starts.
166
+ await yama.initialize(request.configPath, "pr", request);
126
167
  console.log("🚀 Starting autonomous AI review...\n");
127
168
  const result = options.reviewOnly
128
169
  ? await yama.startReview(request)
@@ -140,6 +181,8 @@ function setupReviewCommand() {
140
181
  console.log("\n📄 Full Results:");
141
182
  console.log(JSON.stringify(result, null, 2));
142
183
  }
184
+ // Surface outputs for the composite GitHub Action (provider-agnostic).
185
+ await writeGitHubOutputs(result);
143
186
  process.exit(result.decision === "BLOCKED" ? 1 : 0);
144
187
  }
145
188
  catch (error) {
@@ -152,6 +195,34 @@ function setupReviewCommand() {
152
195
  }
153
196
  });
154
197
  }
198
+ /**
199
+ * Append PR review outputs to the GitHub Actions output file so a composite
200
+ * Action can expose them as step outputs. No-op outside GitHub Actions (when
201
+ * GITHUB_OUTPUT is unset). Provider-agnostic: works for Bitbucket too.
202
+ */
203
+ async function writeGitHubOutputs(result) {
204
+ const outputPath = process.env.GITHUB_OUTPUT;
205
+ if (!outputPath) {
206
+ return;
207
+ }
208
+ const issues = result.statistics?.issuesFound;
209
+ const summary = (result.summary || "").trim();
210
+ const lines = [
211
+ `decision=${result.decision}`,
212
+ `summary<<EOF\n${summary}\nEOF`,
213
+ `critical-issues=${issues?.critical ?? 0}`,
214
+ `major-issues=${issues?.major ?? 0}`,
215
+ `minor-issues=${issues?.minor ?? 0}`,
216
+ `total-comments=${result.statistics?.totalComments ?? 0}`,
217
+ ];
218
+ try {
219
+ const fs = await import("fs/promises");
220
+ await fs.appendFile(outputPath, `${lines.join("\n")}\n`, "utf8");
221
+ }
222
+ catch (error) {
223
+ console.error("⚠️ Failed to write GitHub Action outputs:", error.message);
224
+ }
225
+ }
155
226
  /**
156
227
  * Enhance description command
157
228
  */
package/dist/index.js CHANGED
@@ -5,6 +5,10 @@
5
5
  // ============================================================================
6
6
  // Core Exports
7
7
  // ============================================================================
8
+ // `YamaOrchestrator` is exported below as a class, which exposes it to SDK
9
+ // consumers as BOTH a value and a type (with all of its public methods). A
10
+ // separate `export type { YamaOrchestrator }` is intentionally omitted because
11
+ // it would collide with this value export (TS2300: Duplicate identifier).
8
12
  export { YamaOrchestrator, createYama, YamaOrchestrator as YamaV2Orchestrator, createYama as createYamaV2, } from "./v2/core/YamaV2Orchestrator.js";
9
13
  export { LearningOrchestrator, createLearningOrchestrator, } from "./v2/core/LearningOrchestrator.js";
10
14
  export { ConfigLoader } from "./v2/config/ConfigLoader.js";
@@ -15,9 +15,14 @@ export declare class ConfigLoader {
15
15
  */
16
16
  getConfig(): YamaConfig;
17
17
  /**
18
- * Validate configuration completeness and correctness
18
+ * Validate configuration completeness and correctness.
19
+ *
20
+ * @param mode "pr" requires VCS credentials; "local" is SDK-first and skips them.
21
+ * @param provider Detected VCS provider. When "github", Bitbucket env vars are
22
+ * NOT required (a GitHub token is required instead); otherwise
23
+ * Bitbucket credentials are validated as before.
19
24
  */
20
- validate(mode?: "pr" | "local"): Promise<void>;
25
+ validate(mode?: "pr" | "local", provider?: "github" | "bitbucket"): Promise<void>;
21
26
  /**
22
27
  * Resolve configuration file path
23
28
  */
@@ -53,9 +53,14 @@ export class ConfigLoader {
53
53
  return this.config;
54
54
  }
55
55
  /**
56
- * Validate configuration completeness and correctness
56
+ * Validate configuration completeness and correctness.
57
+ *
58
+ * @param mode "pr" requires VCS credentials; "local" is SDK-first and skips them.
59
+ * @param provider Detected VCS provider. When "github", Bitbucket env vars are
60
+ * NOT required (a GitHub token is required instead); otherwise
61
+ * Bitbucket credentials are validated as before.
57
62
  */
58
- async validate(mode = "pr") {
63
+ async validate(mode = "pr", provider) {
59
64
  if (!this.config) {
60
65
  throw new ConfigurationError("No configuration to validate");
61
66
  }
@@ -69,15 +74,29 @@ export class ConfigLoader {
69
74
  }
70
75
  // Local mode is SDK-first and does not require MCP credentials.
71
76
  if (mode === "pr") {
72
- // Check environment variables for Bitbucket (required in PR mode)
73
- if (!process.env.BITBUCKET_USERNAME) {
74
- errors.push("BITBUCKET_USERNAME environment variable not set");
75
- }
76
- if (!process.env.BITBUCKET_TOKEN) {
77
- errors.push("BITBUCKET_TOKEN environment variable not set");
77
+ if (provider === "github") {
78
+ // GitHub provider: require a GitHub token instead of Bitbucket creds.
79
+ // Token resolution order mirrors MCPServerManager: GITHUB_TOKEN →
80
+ // GH_TOKEN → GITHUB_PERSONAL_ACCESS_TOKEN → GITHUB_ACCESS_TOKEN
81
+ // (GITHUB_ACCESS_TOKEN is the dedicated PAT used in Curator/DNV).
82
+ if (!process.env.GITHUB_TOKEN &&
83
+ !process.env.GH_TOKEN &&
84
+ !process.env.GITHUB_PERSONAL_ACCESS_TOKEN &&
85
+ !process.env.GITHUB_ACCESS_TOKEN) {
86
+ errors.push("GitHub provider selected but no GitHub token found. Set GITHUB_TOKEN (or GH_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN / GITHUB_ACCESS_TOKEN).");
87
+ }
78
88
  }
79
- if (!process.env.BITBUCKET_BASE_URL) {
80
- errors.push("BITBUCKET_BASE_URL environment variable not set");
89
+ else {
90
+ // Bitbucket provider (default): require Bitbucket credentials.
91
+ if (!process.env.BITBUCKET_USERNAME) {
92
+ errors.push("BITBUCKET_USERNAME environment variable not set");
93
+ }
94
+ if (!process.env.BITBUCKET_TOKEN) {
95
+ errors.push("BITBUCKET_TOKEN environment variable not set");
96
+ }
97
+ if (!process.env.BITBUCKET_BASE_URL) {
98
+ errors.push("BITBUCKET_BASE_URL environment variable not set");
99
+ }
81
100
  }
82
101
  if (this.config.mcpServers.jira.enabled) {
83
102
  if (!process.env.JIRA_EMAIL) {
@@ -45,6 +45,19 @@ export class DefaultConfig {
45
45
  bitbucket: {
46
46
  blockedTools: [],
47
47
  },
48
+ github: {
49
+ enabled: true,
50
+ url: "https://api.githubcopilot.com/mcp/",
51
+ transport: "http",
52
+ blockedTools: [
53
+ "push_files",
54
+ "create_or_update_file",
55
+ "create_branch",
56
+ "delete_file",
57
+ "create_pull_request_with_copilot",
58
+ "assign_copilot_to_issue",
59
+ ],
60
+ },
48
61
  jira: {
49
62
  enabled: false, // Opt-in: users must explicitly enable Jira integration
50
63
  blockedTools: [],
@@ -9,6 +9,7 @@ import { ConfigLoader } from "../config/ConfigLoader.js";
9
9
  import { LangfusePromptManager } from "../prompts/LangfusePromptManager.js";
10
10
  import { KnowledgeBaseManager } from "../learning/KnowledgeBaseManager.js";
11
11
  import { MemoryManager } from "../memory/MemoryManager.js";
12
+ import { getProviderToolset, } from "../providers/ProviderToolset.js";
12
13
  import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
13
14
  export class LearningOrchestrator {
14
15
  neurolink;
@@ -157,7 +158,7 @@ export class LearningOrchestrator {
157
158
  for (const [category, items] of byCategory) {
158
159
  parts.push(`${category}: ${items.join("; ")}`);
159
160
  }
160
- const ownerId = MemoryManager.buildOwnerId(request.workspace, request.repository);
161
+ const ownerId = MemoryManager.buildOwnerId(request.workspace, request.repository, request.provider || "bitbucket");
161
162
  await this.neurolink.generate({
162
163
  input: { text: parts.join(". ") },
163
164
  provider: this.config.ai.provider,
@@ -228,17 +229,32 @@ export class LearningOrchestrator {
228
229
  */
229
230
  buildFetchCommentsInstructions(request) {
230
231
  const aiPatterns = this.config.knowledgeBase.aiAuthorPatterns.join(", ");
232
+ // Derive the PR-read tool name and identifier format from the provider
233
+ // toolset so a GitHub learn run asks for the tool it actually has.
234
+ // The learn flow's only provider signal is request.provider; default to
235
+ // bitbucket to preserve existing behavior.
236
+ const provider = request.provider === "github" ? "github" : "bitbucket";
237
+ const toolset = getProviderToolset(provider);
238
+ const prReadTool = toolset.prReadToolName;
239
+ // Bitbucket: workspace/repository/pull_request_id (byte-identical to before).
240
+ // GitHub: owner/repo/pull_number — the same repository identifiers carried
241
+ // by the Bitbucket-shaped request, relabelled to GitHub's vocabulary.
242
+ const prDetails = provider === "github"
243
+ ? ` <owner>${request.workspace}</owner>
244
+ <repo>${request.repository}</repo>
245
+ <pull_number>${request.pullRequestId}</pull_number>`
246
+ : ` <workspace>${request.workspace}</workspace>
247
+ <repository>${request.repository}</repository>
248
+ <pull_request_id>${request.pullRequestId}</pull_request_id>`;
231
249
  return `
232
250
  <task>Fetch and analyze ALL PR comments for learning extraction</task>
233
251
 
234
252
  <pr-details>
235
- <workspace>${request.workspace}</workspace>
236
- <repository>${request.repository}</repository>
237
- <pull_request_id>${request.pullRequestId}</pull_request_id>
253
+ ${prDetails}
238
254
  </pr-details>
239
255
 
240
256
  <instructions>
241
- 1. Use get_pull_request tool to fetch the PR details including all comments/activities
257
+ 1. Use ${prReadTool} tool to fetch the PR details including all comments/activities
242
258
  2. Look through ALL comments on the PR (both active and resolved if available)
243
259
  3. Identify AI-generated comments by:
244
260
  - Author patterns: ${aiPatterns}
@@ -6,10 +6,21 @@ import { MCPServersConfig } from "../types/config.types.js";
6
6
  export declare class MCPServerManager {
7
7
  private initialized;
8
8
  /**
9
- * Setup all MCP servers in NeuroLink
10
- * Bitbucket is always enabled, Jira is optional based on config
9
+ * Setup MCP servers based on detected provider
10
+ * GitHub MCP OR Bitbucket MCP (not both) - whichever is needed
11
+ * Jira is optional for both
11
12
  */
12
- setupMCPServers(neurolink: any, config: MCPServersConfig): Promise<void>;
13
+ setupMCPServers(neurolink: any, config: MCPServersConfig, provider?: "github" | "bitbucket"): Promise<void>;
14
+ /**
15
+ * Reset PR-mode MCP setup so {@link setupMCPServers} can re-register for a
16
+ * different provider within the same process (e.g. one orchestrator instance
17
+ * reviewing a Bitbucket PR then a GitHub PR). Removes the previously-registered
18
+ * provider server (Bitbucket or GitHub) so the new provider's server takes its
19
+ * place, and clears the `initialized` guard. The optional Jira server is left
20
+ * in place — it is provider-agnostic and shared. Single-provider runs never
21
+ * call this, so their behaviour is unchanged.
22
+ */
23
+ resetForProviderSwitch(neurolink: any, previousProvider: "github" | "bitbucket"): Promise<void>;
13
24
  /**
14
25
  * Setup local git MCP server for SDK/local mode.
15
26
  * Mandatory in local mode.
@@ -22,6 +33,36 @@ export declare class MCPServerManager {
22
33
  * Setup Bitbucket MCP server (hardcoded, always enabled)
23
34
  */
24
35
  private setupBitbucketMCP;
36
+ /**
37
+ * Setup GitHub MCP server.
38
+ *
39
+ * Registers GitHub's hosted REMOTE HTTP MCP server via NeuroLink, mirroring
40
+ * Curator's proven pattern (transport: "http" + Bearer auth header). The old
41
+ * `npx @github/github-mcp-server` package does not exist, so stdio is only
42
+ * supported for an explicitly-configured self-hosted / Docker server.
43
+ *
44
+ * URL + transport are config-driven (`mcpServers.github.{url,transport,command,args}`),
45
+ * defaulting to the remote HTTP endpoint.
46
+ */
47
+ private setupGitHubMCP;
48
+ /**
49
+ * Poll {@link listMCPServers} until the named server reports "connected" with
50
+ * at least one tool, or the timeout elapses. Returns the last-seen server
51
+ * entry (possibly not-yet-connected) so the caller can decide how to proceed.
52
+ *
53
+ * Bounded by `timeoutMs` via the loop condition — it cannot run indefinitely.
54
+ */
55
+ private waitForMCPServer;
56
+ /**
57
+ * Build the remote HTTP GitHub MCP registration (default path).
58
+ * A Bearer token is required for the hosted endpoint.
59
+ */
60
+ private buildGitHubHttpConfig;
61
+ /**
62
+ * Build a self-hosted / Docker stdio GitHub MCP registration.
63
+ * Only used when `mcpServers.github.transport === "stdio"`; requires `command`.
64
+ */
65
+ private buildGitHubStdioConfig;
25
66
  /**
26
67
  * Setup Jira MCP server (hardcoded, optionally enabled)
27
68
  */
@@ -4,22 +4,52 @@
4
4
  */
5
5
  import { join } from "path";
6
6
  import { MCPServerError } from "../types/v2.types.js";
7
+ /**
8
+ * Default remote HTTP endpoint for GitHub's hosted MCP server.
9
+ * Overridable via `mcpServers.github.url`.
10
+ */
11
+ const DEFAULT_GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/";
12
+ /**
13
+ * Default write tools blocked on the GitHub MCP server.
14
+ * Mirrors Curator's proven pattern: the AI never mutates the repo directly —
15
+ * the single write path is the review-comment / review-submit tools, which are
16
+ * intentionally NOT blocked here.
17
+ */
18
+ const DEFAULT_GITHUB_BLOCKED_TOOLS = [
19
+ "push_files",
20
+ "create_or_update_file",
21
+ "create_branch",
22
+ "delete_file",
23
+ "create_pull_request_with_copilot",
24
+ "assign_copilot_to_issue",
25
+ ];
7
26
  export class MCPServerManager {
8
27
  // MCP servers are managed entirely by NeuroLink
9
28
  // No need to track tools locally
10
29
  initialized = false;
11
30
  /**
12
- * Setup all MCP servers in NeuroLink
13
- * Bitbucket is always enabled, Jira is optional based on config
31
+ * Setup MCP servers based on detected provider
32
+ * GitHub MCP OR Bitbucket MCP (not both) - whichever is needed
33
+ * Jira is optional for both
14
34
  */
15
- async setupMCPServers(neurolink, config) {
35
+ async setupMCPServers(neurolink, config, provider = "bitbucket") {
16
36
  if (this.initialized) {
17
37
  return;
18
38
  }
19
39
  console.log("🔌 Setting up MCP servers...");
20
- // Setup Bitbucket MCP (always enabled)
21
- await this.setupBitbucketMCP(neurolink, config.bitbucket?.blockedTools);
22
- // Setup Jira MCP (optional)
40
+ console.log(` 📍 Provider: ${provider}`);
41
+ // Setup provider-specific MCP (only one needed)
42
+ if (provider === "github") {
43
+ // Fail fast rather than passing an undefined/disabled config downstream.
44
+ if (!config.github || config.github.enabled === false) {
45
+ throw new MCPServerError("GitHub provider selected but mcpServers.github is not enabled");
46
+ }
47
+ await this.setupGitHubMCP(neurolink, config.github);
48
+ }
49
+ else {
50
+ await this.setupBitbucketMCP(neurolink, config.bitbucket?.blockedTools);
51
+ }
52
+ // Setup Jira MCP (optional, works with both providers)
23
53
  if (config.jira.enabled) {
24
54
  await this.setupJiraMCP(neurolink, config.jira.blockedTools);
25
55
  }
@@ -30,6 +60,26 @@ export class MCPServerManager {
30
60
  await this.logDiagnostics(neurolink);
31
61
  console.log("✅ MCP servers configured\n");
32
62
  }
63
+ /**
64
+ * Reset PR-mode MCP setup so {@link setupMCPServers} can re-register for a
65
+ * different provider within the same process (e.g. one orchestrator instance
66
+ * reviewing a Bitbucket PR then a GitHub PR). Removes the previously-registered
67
+ * provider server (Bitbucket or GitHub) so the new provider's server takes its
68
+ * place, and clears the `initialized` guard. The optional Jira server is left
69
+ * in place — it is provider-agnostic and shared. Single-provider runs never
70
+ * call this, so their behaviour is unchanged.
71
+ */
72
+ async resetForProviderSwitch(neurolink, previousProvider) {
73
+ const serverName = previousProvider === "github" ? "github" : "bitbucket";
74
+ try {
75
+ await neurolink.removeExternalMCPServer?.(serverName);
76
+ }
77
+ catch {
78
+ // Non-fatal: if removal fails the subsequent re-register will surface a
79
+ // clearer error. Proceed to allow re-setup.
80
+ }
81
+ this.initialized = false;
82
+ }
33
83
  /**
34
84
  * Setup local git MCP server for SDK/local mode.
35
85
  * Mandatory in local mode.
@@ -145,6 +195,154 @@ export class MCPServerManager {
145
195
  throw new MCPServerError(`Failed to setup Bitbucket MCP server: ${error.message}`);
146
196
  }
147
197
  }
198
+ /**
199
+ * Setup GitHub MCP server.
200
+ *
201
+ * Registers GitHub's hosted REMOTE HTTP MCP server via NeuroLink, mirroring
202
+ * Curator's proven pattern (transport: "http" + Bearer auth header). The old
203
+ * `npx @github/github-mcp-server` package does not exist, so stdio is only
204
+ * supported for an explicitly-configured self-hosted / Docker server.
205
+ *
206
+ * URL + transport are config-driven (`mcpServers.github.{url,transport,command,args}`),
207
+ * defaulting to the remote HTTP endpoint.
208
+ */
209
+ async setupGitHubMCP(neurolink, githubConfig) {
210
+ try {
211
+ console.log(" 🔧 Registering GitHub MCP server...");
212
+ // Be robust to a possibly-undefined config: default URL + transport +
213
+ // blockedTools so registration still works if the caller passes nothing.
214
+ const transport = githubConfig?.transport ?? "http";
215
+ // ADDITIVE, not replacing: always enforce the default write-block denylist
216
+ // and union it with any user-provided entries, so a user-supplied
217
+ // `blockedTools` can only EXTEND the denylist — never un-block write tools
218
+ // like push_files / create_or_update_file.
219
+ const blockedTools = Array.from(new Set([
220
+ ...DEFAULT_GITHUB_BLOCKED_TOOLS,
221
+ ...(githubConfig?.blockedTools ?? []),
222
+ ]));
223
+ // An explicit token is REQUIRED. gh CLI auth (hosts.yml) cannot supply a
224
+ // Bearer token for the remote HTTP transport, so we do not accept it as a
225
+ // substitute. The hosted endpoint (api.githubcopilot.com) expects a real
226
+ // GitHub PAT — the ephemeral Actions GITHUB_TOKEN may be rejected. Token
227
+ // resolution order mirrors Curator's proven setup (GITHUB_ACCESS_TOKEN):
228
+ // GITHUB_TOKEN → GH_TOKEN → GITHUB_PERSONAL_ACCESS_TOKEN → GITHUB_ACCESS_TOKEN.
229
+ const ghToken = process.env.GITHUB_TOKEN ||
230
+ process.env.GH_TOKEN ||
231
+ process.env.GITHUB_PERSONAL_ACCESS_TOKEN ||
232
+ process.env.GITHUB_ACCESS_TOKEN;
233
+ if (!ghToken) {
234
+ throw new MCPServerError("Missing GitHub authentication: set GITHUB_TOKEN (or GH_TOKEN / " +
235
+ "GITHUB_PERSONAL_ACCESS_TOKEN / GITHUB_ACCESS_TOKEN). The remote HTTP " +
236
+ "transport requires an explicit GitHub PAT; 'gh auth login' alone is " +
237
+ "not sufficient.");
238
+ }
239
+ const config = transport === "stdio"
240
+ ? this.buildGitHubStdioConfig(githubConfig, ghToken, blockedTools)
241
+ : this.buildGitHubHttpConfig(githubConfig, ghToken, blockedTools);
242
+ await neurolink.addExternalMCPServer("github", config);
243
+ // The remote HTTP endpoint (api.githubcopilot.com) connects asynchronously
244
+ // — TLS handshake + auth + tool discovery take ~3-4s, and NeuroLink may only
245
+ // finish discovery lazily on the first generate() call. Poll briefly for a
246
+ // healthy status, but for the HTTP transport DO NOT hard-fail if it is not
247
+ // yet "connected": the tools are resolved when the review runs (this mirrors
248
+ // Curator's proven pattern). For a local stdio server we keep the strict
249
+ // check, since a local process should connect promptly.
250
+ const ghServer = await this.waitForMCPServer(neurolink, "github", 12000);
251
+ const connected = ghServer?.status === "connected" && (ghServer?.tools?.length ?? 0) > 0;
252
+ if (transport === "stdio" && !connected) {
253
+ throw new MCPServerError(`GitHub MCP server registered but not connected (status: ${ghServer?.status ?? "unknown"}, tools: ${ghServer?.tools?.length ?? 0}). Possible startup timeout.`);
254
+ }
255
+ if (connected) {
256
+ console.log(` ✅ GitHub MCP server registered and tools available (transport: ${transport})`);
257
+ }
258
+ else {
259
+ console.warn(` ⚠️ GitHub MCP server registered but not yet reporting connected ` +
260
+ `(status: ${ghServer?.status ?? "unknown"}, tools: ${ghServer?.tools?.length ?? 0}). ` +
261
+ `Remote tools are discovered on first use; continuing.`);
262
+ }
263
+ if (transport === "http") {
264
+ console.log(` 🌐 Remote endpoint: ${config.url}`);
265
+ }
266
+ console.log(` 🚫 Blocked write tools: ${blockedTools.join(", ")}`);
267
+ }
268
+ catch (error) {
269
+ throw new MCPServerError(`Failed to setup GitHub MCP server: ${error.message}`);
270
+ }
271
+ }
272
+ /**
273
+ * Poll {@link listMCPServers} until the named server reports "connected" with
274
+ * at least one tool, or the timeout elapses. Returns the last-seen server
275
+ * entry (possibly not-yet-connected) so the caller can decide how to proceed.
276
+ *
277
+ * Bounded by `timeoutMs` via the loop condition — it cannot run indefinitely.
278
+ */
279
+ async waitForMCPServer(neurolink, name, timeoutMs) {
280
+ const deadline = Date.now() + timeoutMs;
281
+ let last;
282
+ while (Date.now() < deadline) {
283
+ const servers = await neurolink
284
+ .listMCPServers()
285
+ .catch(() => []);
286
+ last = (servers || []).find((server) => server.name === name);
287
+ if (last?.status === "connected" && (last.tools?.length ?? 0) > 0) {
288
+ return last;
289
+ }
290
+ await new Promise((resolve) => setTimeout(resolve, 1000));
291
+ }
292
+ return last;
293
+ }
294
+ /**
295
+ * Build the remote HTTP GitHub MCP registration (default path).
296
+ * A Bearer token is required for the hosted endpoint.
297
+ */
298
+ buildGitHubHttpConfig(githubConfig, ghToken, blockedTools) {
299
+ if (!ghToken) {
300
+ throw new MCPServerError("GitHub remote HTTP MCP server requires a token. Set GITHUB_TOKEN " +
301
+ "(or GH_TOKEN / GITHUB_PERSONAL_ACCESS_TOKEN / GITHUB_ACCESS_TOKEN); " +
302
+ "'gh auth login' alone is not sufficient for the remote transport.");
303
+ }
304
+ // The hosted endpoint is slow to connect (~3-4s TLS handshake + remote auth);
305
+ // a generous timeout + retry/backoff prevents spurious "not connected"
306
+ // failures. Values mirror Curator's production GitHub MCP registration.
307
+ return {
308
+ transport: "http",
309
+ url: githubConfig?.url ?? DEFAULT_GITHUB_MCP_URL,
310
+ headers: {
311
+ Authorization: `Bearer ${ghToken}`,
312
+ },
313
+ blockedTools,
314
+ timeout: 30000,
315
+ retryConfig: {
316
+ maxAttempts: 3,
317
+ initialDelay: 1000,
318
+ maxDelay: 10000,
319
+ backoffMultiplier: 2,
320
+ },
321
+ };
322
+ }
323
+ /**
324
+ * Build a self-hosted / Docker stdio GitHub MCP registration.
325
+ * Only used when `mcpServers.github.transport === "stdio"`; requires `command`.
326
+ */
327
+ buildGitHubStdioConfig(githubConfig, ghToken, blockedTools) {
328
+ const command = githubConfig?.command;
329
+ if (!command) {
330
+ throw new MCPServerError("GitHub stdio transport requires 'mcpServers.github.command' " +
331
+ "(e.g. a self-hosted/Docker GitHub MCP server binary).");
332
+ }
333
+ return {
334
+ transport: "stdio",
335
+ command,
336
+ args: githubConfig?.args ?? [],
337
+ env: ghToken
338
+ ? {
339
+ GITHUB_PERSONAL_ACCESS_TOKEN: ghToken,
340
+ GITHUB_TOKEN: ghToken,
341
+ }
342
+ : undefined,
343
+ blockedTools,
344
+ };
345
+ }
148
346
  /**
149
347
  * Setup Jira MCP server (hardcoded, optionally enabled)
150
348
  */
@@ -16,16 +16,18 @@ export declare class YamaOrchestrator {
16
16
  private config;
17
17
  private initialized;
18
18
  private mcpInitialized;
19
+ private mcpProvider;
19
20
  private localGitMcpInitialized;
20
21
  private exploreToolRegistered;
21
22
  private currentToolContext;
22
23
  private initOptions;
23
24
  private bootstrapStandardsCache;
25
+ private detectedProvider;
24
26
  constructor(options?: YamaInitOptions);
25
27
  /**
26
28
  * Initialize Yama with configuration and MCP servers
27
29
  */
28
- initialize(configPath?: string, mode?: ReviewMode): Promise<void>;
30
+ initialize(configPath?: string, mode?: ReviewMode, request?: ReviewRequest): Promise<void>;
29
31
  /**
30
32
  * Start autonomous AI review
31
33
  */
@@ -97,6 +99,23 @@ export declare class YamaOrchestrator {
97
99
  * Calculate statistics from session
98
100
  */
99
101
  private calculateStatistics;
102
+ /**
103
+ * Count the changed files actually reviewed, from the session tool calls.
104
+ *
105
+ * Bitbucket: get_pull_request_diff is called once per file by design, so the
106
+ * raw count of diff-tool calls already equals the files reviewed. This path is
107
+ * byte-for-byte identical to the previous behaviour.
108
+ *
109
+ * GitHub: pull_request_read is overloaded — the SAME tool name serves
110
+ * method="get" (PR shell), "get_files" (changed-file list) and "get_diff"
111
+ * (unified diff). Counting every pull_request_read call therefore inflates the
112
+ * number. Instead, count DISTINCT file paths the review actually touched, taken
113
+ * from the inline pending-review comments (add_comment_to_pending_review →
114
+ * args.path). Fall back to the number of distinct get_files/get_diff reads when
115
+ * no inline comments were posted (e.g. a clean approval), so a reviewed-but-
116
+ * clean PR is not reported as 0 files.
117
+ */
118
+ private countFilesReviewed;
100
119
  /**
101
120
  * Extract issue counts from comment tool calls
102
121
  */