@juspay/yama 2.6.0 → 2.7.1

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.7.1](https://github.com/juspay/yama/compare/v2.7.0...v2.7.1) (2026-06-15)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **v2:** bump neurolink to 9.70.7 and fix review/config correctness bugs ([d0c62f7](https://github.com/juspay/yama/commit/d0c62f767bb3f38fed0bc5dcc1a64845529ec3b6))
7
+
8
+ # [2.7.0](https://github.com/juspay/yama/compare/v2.6.0...v2.7.0) (2026-06-11)
9
+
10
+
11
+ ### Features
12
+
13
+ * **setup:** add one-command GitHub integration setup script ([6a014b0](https://github.com/juspay/yama/commit/6a014b05ccf12e4ca362316e5f69c9441a93618f))
14
+
1
15
  # [2.6.0](https://github.com/juspay/yama/compare/v2.5.0...v2.6.0) (2026-06-04)
2
16
 
3
17
 
package/dist/index.d.ts CHANGED
@@ -12,6 +12,6 @@ export { MemoryManager } from "./v2/memory/MemoryManager.js";
12
12
  export type { LocalReviewFinding, LocalReviewRequest, LocalReviewResult, ReviewRequest, ReviewMode, ReviewResult, ReviewUpdate, ReviewSession, ReviewStatistics, IssuesBySeverity, TokenUsage, UnifiedReviewRequest, } from "./v2/types/v2.types.js";
13
13
  export type { YamaConfig, YamaInitOptions, YamaConfig as YamaV2Config, AIConfig, MCPServersConfig, ReviewConfig, DescriptionEnhancementConfig, MemoryConfig, } from "./v2/types/config.types.js";
14
14
  export type { GetPullRequestResponse, GetPullRequestDiffResponse, GetIssueResponse, SearchCodeResponse, } from "./v2/types/mcp.types.js";
15
- export declare const VERSION = "2.2.1";
15
+ export declare const VERSION: string;
16
16
  export { createYama as default } from "./v2/core/YamaV2Orchestrator.js";
17
17
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Yama - AI-Native Code Review
3
3
  * Main export file
4
4
  */
5
+ import { createRequire } from "node:module";
5
6
  // ============================================================================
6
7
  // Core Exports
7
8
  // ============================================================================
@@ -19,7 +20,26 @@ export { MemoryManager } from "./v2/memory/MemoryManager.js";
19
20
  // ============================================================================
20
21
  // Version Information
21
22
  // ============================================================================
22
- export const VERSION = "2.2.1";
23
+ /**
24
+ * Single source of truth for the Yama version: read from package.json at runtime
25
+ * (this module sits one level below package.json in both src/ and dist/, so the
26
+ * relative require resolves in dev and in the published build). Falls back to a
27
+ * sentinel if the file can't be read, instead of drifting from a hardcoded
28
+ * literal.
29
+ */
30
+ function resolveVersion() {
31
+ try {
32
+ const require = createRequire(import.meta.url);
33
+ const pkg = require("../package.json");
34
+ return typeof pkg.version === "string" && pkg.version.length > 0
35
+ ? pkg.version
36
+ : "0.0.0";
37
+ }
38
+ catch {
39
+ return "0.0.0";
40
+ }
41
+ }
42
+ export const VERSION = resolveVersion();
23
43
  // ============================================================================
24
44
  // Default Export
25
45
  // ============================================================================
@@ -16,12 +16,16 @@ export class ConfigLoader {
16
16
  */
17
17
  async loadConfig(configPath, instanceOverrides) {
18
18
  console.log("📋 Loading Yama configuration...");
19
+ // Final precedence (lowest → highest):
20
+ // defaults < file < env < instance overrides
21
+ // Environment variables intentionally override the committed config file
22
+ // (the standard expectation). Env overrides are re-applied AFTER the file
23
+ // merge; they are idempotent and only mutate values when the env var is
24
+ // actually set (see applyEnvironmentOverrides / applyMemoryOverrides), so
25
+ // they never clobber file values with undefined.
19
26
  // Layer 1: Start with default config
20
27
  let config = DefaultConfig.get();
21
- // Layer 2: Apply environment variable overrides
22
- // Lowest user-provided layer in SDK mode precedence.
23
- config = this.applyEnvironmentOverrides(config);
24
- // Layer 3: Load from file if provided or search for default locations
28
+ // Layer 2: Load from file if provided or search for default locations
25
29
  const filePath = await this.resolveConfigPath(configPath);
26
30
  if (filePath) {
27
31
  console.log(` Reading config from: ${filePath}`);
@@ -32,6 +36,9 @@ export class ConfigLoader {
32
36
  else {
33
37
  console.log(" Using default configuration (no config file found)");
34
38
  }
39
+ // Layer 3: Apply environment variable overrides AFTER the file so env wins
40
+ // over the committed config (consistently for both ai.* and memory.*).
41
+ config = this.applyEnvironmentOverrides(config);
35
42
  config.memory = this.applyMemoryOverrides(config.memory);
36
43
  // Layer 4: Apply SDK instance overrides (highest priority)
37
44
  if (instanceOverrides) {
@@ -17,7 +17,8 @@ export class DefaultConfig {
17
17
  provider: "auto",
18
18
  model: "gemini-2.5-pro",
19
19
  temperature: 0.2,
20
- maxTokens: 128000,
20
+ // Sane output ceiling (NeuroLink clamps further per-model server-side).
21
+ maxTokens: 32000,
21
22
  enableAnalytics: true,
22
23
  enableEvaluation: false,
23
24
  timeout: "15m",
@@ -36,7 +37,8 @@ export class DefaultConfig {
36
37
  provider: "auto",
37
38
  model: "gemini-2.5-flash",
38
39
  temperature: 0.1,
39
- maxTokens: 32000,
40
+ // Lighter sub-task: sane output ceiling (NeuroLink clamps further per-model server-side).
41
+ maxTokens: 16000,
40
42
  timeout: "5m",
41
43
  cacheResults: true,
42
44
  },
@@ -34,6 +34,17 @@ export declare class LearningOrchestrator {
34
34
  * Parse structured response from AI output
35
35
  */
36
36
  private parseStructuredLearnings;
37
+ /**
38
+ * Validate that a candidate value (typically `response.structuredData`) is a
39
+ * usable learning-extraction object. Returns it typed when it carries a
40
+ * `learnings` array, otherwise null so callers fall back to text parsing.
41
+ */
42
+ private coerceLearningOutput;
43
+ /**
44
+ * Fallback parser: extract the learning-extraction JSON from the raw response
45
+ * text (may be wrapped in a ```json code block).
46
+ */
47
+ private parseLearningOutputFromText;
37
48
  /**
38
49
  * Initialize NeuroLink with observability
39
50
  */
@@ -11,6 +11,7 @@ import { KnowledgeBaseManager } from "../learning/KnowledgeBaseManager.js";
11
11
  import { MemoryManager } from "../memory/MemoryManager.js";
12
12
  import { getProviderToolset, } from "../providers/ProviderToolset.js";
13
13
  import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
14
+ import { clampMaxTokens } from "../utils/tokenLimits.js";
14
15
  export class LearningOrchestrator {
15
16
  neurolink;
16
17
  mcpManager;
@@ -77,7 +78,7 @@ export class LearningOrchestrator {
77
78
  provider: this.config.ai.provider,
78
79
  model: this.config.ai.model,
79
80
  temperature: 0.1,
80
- maxTokens: 50000,
81
+ maxTokens: clampMaxTokens(50000),
81
82
  timeout: this.config.ai.timeout,
82
83
  context: {
83
84
  operation: "learning-fetch-comments",
@@ -96,7 +97,7 @@ export class LearningOrchestrator {
96
97
  provider: this.config.ai.provider,
97
98
  model: this.config.ai.model,
98
99
  temperature: 0.2,
99
- maxTokens: 10000,
100
+ maxTokens: clampMaxTokens(10000),
100
101
  timeout: "2m",
101
102
  disableTools: true, // No tools needed for extraction phase
102
103
  context: {
@@ -164,7 +165,7 @@ export class LearningOrchestrator {
164
165
  provider: this.config.ai.provider,
165
166
  model: this.config.ai.model,
166
167
  temperature: 0.1,
167
- maxTokens: 100,
168
+ maxTokens: clampMaxTokens(100),
168
169
  disableTools: true,
169
170
  context: {
170
171
  userId: ownerId,
@@ -377,16 +378,15 @@ Analyze the PR data above and extract learnings.
377
378
  */
378
379
  parseStructuredLearnings(response, prId) {
379
380
  try {
380
- const content = (response.content || "");
381
- // Find JSON in response (may be in code block)
382
- const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/) ||
383
- content.match(/\{[\s\S]*\}/);
384
- if (!jsonMatch) {
385
- console.log(" ⚠️ No JSON found in extraction response");
381
+ // Prefer the provider's structured output (NeuroLink populates
382
+ // `structuredData` when the model returns a parsed object) before falling
383
+ // back to hand-parsing the raw text. This avoids brittle regex extraction
384
+ // when a clean object is already available.
385
+ const structured = this.coerceLearningOutput(response.structuredData);
386
+ const parsed = structured ?? this.parseLearningOutputFromText(response);
387
+ if (!parsed) {
386
388
  return [];
387
389
  }
388
- const jsonStr = jsonMatch[1] || jsonMatch[0];
389
- const parsed = JSON.parse(jsonStr);
390
390
  // Log summary if available
391
391
  if (parsed.summary) {
392
392
  const devComments = parsed.summary.totalIndependentDevComments || 0;
@@ -413,6 +413,51 @@ Analyze the PR data above and extract learnings.
413
413
  return [];
414
414
  }
415
415
  }
416
+ /**
417
+ * Validate that a candidate value (typically `response.structuredData`) is a
418
+ * usable learning-extraction object. Returns it typed when it carries a
419
+ * `learnings` array, otherwise null so callers fall back to text parsing.
420
+ */
421
+ coerceLearningOutput(candidate) {
422
+ if (candidate &&
423
+ typeof candidate === "object" &&
424
+ Array.isArray(candidate.learnings)) {
425
+ return candidate;
426
+ }
427
+ return null;
428
+ }
429
+ /**
430
+ * Fallback parser: extract the learning-extraction JSON from the raw response
431
+ * text (may be wrapped in a ```json code block).
432
+ */
433
+ parseLearningOutputFromText(response) {
434
+ const content = (response.content || "");
435
+ // Extract JSON without backtracking-prone regexes (ReDoS-safe): locate the
436
+ // fenced ```json block (or the first '{' .. last '}' span) via linear string
437
+ // scans instead of polynomial regex matching on uncontrolled model output.
438
+ let jsonStr = null;
439
+ const fenceToken = "```json";
440
+ const fenceStart = content.indexOf(fenceToken);
441
+ if (fenceStart !== -1) {
442
+ const afterFence = fenceStart + fenceToken.length;
443
+ const fenceEnd = content.indexOf("```", afterFence);
444
+ if (fenceEnd !== -1) {
445
+ jsonStr = content.slice(afterFence, fenceEnd).trim();
446
+ }
447
+ }
448
+ if (!jsonStr) {
449
+ const objStart = content.indexOf("{");
450
+ const objEnd = content.lastIndexOf("}");
451
+ if (objStart !== -1 && objEnd > objStart) {
452
+ jsonStr = content.slice(objStart, objEnd + 1);
453
+ }
454
+ }
455
+ if (!jsonStr) {
456
+ console.log(" ⚠️ No JSON found in extraction response");
457
+ return null;
458
+ }
459
+ return JSON.parse(jsonStr);
460
+ }
416
461
  // ============================================================================
417
462
  // Private Methods
418
463
  // ============================================================================
@@ -513,7 +558,7 @@ Consolidate the learnings as instructed. Return the complete updated knowledge b
513
558
  provider: this.config.ai.provider,
514
559
  model: this.config.ai.model,
515
560
  temperature: 0.2,
516
- maxTokens: 30000,
561
+ maxTokens: clampMaxTokens(30000),
517
562
  });
518
563
  // Parse and update the knowledge base
519
564
  const newContent = (response.content || "");
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { join } from "path";
6
6
  import { MCPServerError } from "../types/v2.types.js";
7
+ import { isMutatingGitTool as isMutatingGitToolShared } from "../utils/toolPolicy.js";
7
8
  /**
8
9
  * Default remote HTTP endpoint for GitHub's hosted MCP server.
9
10
  * Overridable via `mcpServers.github.url`.
@@ -146,9 +147,12 @@ export class MCPServerManager {
146
147
  .filter((name) => typeof name === "string");
147
148
  }
148
149
  isMutatingGitTool(toolName) {
149
- // Handles plain names (git_commit) and prefixed names (local-git.git_commit).
150
- const normalized = toolName.split(/[.:/]/).pop() || toolName;
151
- return /^git_(commit|push|add|checkout|create_branch|merge|rebase|cherry_pick|reset|revert|tag|rm|clean|stash|apply)\b/i.test(normalized);
150
+ // Delegate to the shared fail-closed allow-list policy: any git_* tool not
151
+ // on the read-only allow-list is treated as mutating. This closes the gaps
152
+ // the old regex blocklist missed (git_branch, git_mv, git_pull, git_fetch,
153
+ // git_restore, git_switch, git_remote). Prefix handling (local-git.git_*)
154
+ // is performed inside the shared helper.
155
+ return isMutatingGitToolShared(toolName);
152
156
  }
153
157
  /**
154
158
  * Setup Bitbucket MCP server (hardcoded, always enabled)
@@ -2,7 +2,7 @@
2
2
  * Yama Orchestrator
3
3
  * Main entry point for AI-native autonomous code review
4
4
  */
5
- import { LocalReviewRequest, LocalReviewResult, ReviewRequest, ReviewResult, ReviewMode, ReviewUpdate, UnifiedReviewRequest } from "../types/v2.types.js";
5
+ import { LocalReviewRequest, LocalReviewResult, ReviewRequest, ReviewResult, ReviewMode, UnifiedReviewRequest } from "../types/v2.types.js";
6
6
  import { YamaInitOptions } from "../types/config.types.js";
7
7
  export declare class YamaOrchestrator {
8
8
  private neurolink;
@@ -40,10 +40,6 @@ export declare class YamaOrchestrator {
40
40
  * Local SDK mode review from git diff (no PR/MCP dependency).
41
41
  */
42
42
  reviewLocalDiff(request: LocalReviewRequest): Promise<LocalReviewResult>;
43
- /**
44
- * Stream review with real-time updates (for verbose mode)
45
- */
46
- streamReview(request: ReviewRequest): AsyncIterableIterator<ReviewUpdate>;
47
43
  /**
48
44
  * Start review and then enhance description in the same session
49
45
  * This allows the AI to use knowledge gained during review to write better descriptions
@@ -75,6 +71,21 @@ export declare class YamaOrchestrator {
75
71
  private createToolContext;
76
72
  private createLocalToolContext;
77
73
  private setToolContext;
74
+ /**
75
+ * Run a NeuroLink generate() call with a small bounded retry on transient
76
+ * errors (wires up the previously-unused `ai.retryAttempts` config). Retries
77
+ * up to `retryAttempts` times (>=1 total attempt) with a short exponential
78
+ * backoff. Non-transient errors (e.g. auth/validation) fail fast on the first
79
+ * attempt so we don't paper over real misconfiguration.
80
+ */
81
+ private generateWithRetry;
82
+ /**
83
+ * Heuristic transient-error classifier for retry decisions. Conservative:
84
+ * matches common network / rate-limit / timeout / 5xx signals and leaves
85
+ * everything else (auth, validation, 4xx) as non-retryable.
86
+ */
87
+ private isTransientError;
88
+ private delay;
78
89
  /**
79
90
  * Parse AI response into structured review result
80
91
  */
@@ -89,6 +100,12 @@ export declare class YamaOrchestrator {
89
100
  private extractJsonPayload;
90
101
  private normalizeFindings;
91
102
  private normalizeSeverity;
103
+ /**
104
+ * Map an arbitrary value to a known severity, or null if it is not one of the
105
+ * recognised levels. Unlike normalizeSeverity this does NOT default to MINOR,
106
+ * so callers can distinguish "unknown" from a real level.
107
+ */
108
+ private coerceSeverity;
92
109
  private countFindingsBySeverity;
93
110
  private normalizeDecision;
94
111
  /**
@@ -125,7 +142,14 @@ export declare class YamaOrchestrator {
125
142
  */
126
143
  private extractSummary;
127
144
  /**
128
- * Calculate cost estimate from token usage
145
+ * Calculate cost estimate from an AI response.
146
+ *
147
+ * Prefers NeuroLink's own analytics cost (`response.analytics.cost`, a USD
148
+ * number computed per-provider/model when enableAnalytics is true — Yama sets
149
+ * it). The previous implementation hardcoded Gemini-2.0-Flash pricing
150
+ * ($0.25/$1.00 per 1M) for EVERY provider, which is ~12-15x too low for Claude.
151
+ * Only when the analytics cost is unavailable do we fall back to a clearly
152
+ * labeled rough estimate based on the { input, output } token usage.
129
153
  */
130
154
  private calculateCost;
131
155
  private toSafeNumber;
@@ -137,7 +161,11 @@ export declare class YamaOrchestrator {
137
161
  private getPRToolFilteringOptions;
138
162
  /**
139
163
  * Query-level read-only filtering for local-git MCP tools.
140
- * Uses regex-based mutation detection instead of hardcoded tool-name lists.
164
+ *
165
+ * Uses the shared, fail-closed `isMutatingGitTool` helper (any git_* tool not
166
+ * on the read-only allow-list is treated as mutating) so the orchestrator,
167
+ * explorer, and MCP manager all agree on what is read-only. The previous local
168
+ * regex silently missed mutating tools like git_branch / git_mv / git_pull.
141
169
  */
142
170
  private getLocalToolFilteringOptions;
143
171
  private normalizeToolName;
@@ -13,6 +13,9 @@ import { ContextExplorerService } from "../exploration/ContextExplorerService.js
13
13
  import { ProviderDetector, } from "../utils/ProviderDetector.js";
14
14
  import { getProviderToolset } from "../providers/ProviderToolset.js";
15
15
  import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
16
+ import { clampMaxTokens } from "../utils/tokenLimits.js";
17
+ import { isMutatingGitTool } from "../utils/toolPolicy.js";
18
+ import { VERSION } from "../../index.js";
16
19
  export class YamaOrchestrator {
17
20
  neurolink;
18
21
  explorer = null;
@@ -153,13 +156,14 @@ export class YamaOrchestrator {
153
156
  // Execute autonomous AI review
154
157
  console.log("🤖 Starting autonomous AI review...");
155
158
  console.log(" AI will now make decisions and execute actions autonomously\n");
156
- // Review call: RETRIEVE memory (for context), but DON'T STORE
157
- const aiResponse = await this.neurolink.generate({
159
+ // Review call: RETRIEVE memory (for context), but DON'T STORE.
160
+ // Wrapped in a bounded retry (ai.retryAttempts) for transient failures.
161
+ const aiResponse = await this.generateWithRetry(() => this.neurolink.generate({
158
162
  input: { text: instructions },
159
163
  provider: this.config.ai.provider,
160
164
  model: this.config.ai.model,
161
165
  temperature: this.config.ai.temperature,
162
- maxTokens: this.config.ai.maxTokens,
166
+ maxTokens: clampMaxTokens(this.config.ai.maxTokens),
163
167
  timeout: this.config.ai.timeout,
164
168
  skipToolPromptInjection: true,
165
169
  ...this.getPRToolFilteringOptions(instructions),
@@ -172,7 +176,7 @@ export class YamaOrchestrator {
172
176
  memory: { read: true, write: false },
173
177
  enableAnalytics: this.config.ai.enableAnalytics,
174
178
  enableEvaluation: this.config.ai.enableEvaluation,
175
- });
179
+ }), "code-review");
176
180
  this.recordToolCallsFromResponse(sessionId, aiResponse);
177
181
  // Extract and parse results
178
182
  const result = this.parseReviewResult(aiResponse, startTime, sessionId);
@@ -217,12 +221,12 @@ export class YamaOrchestrator {
217
221
  const instructions = await this.promptBuilder.buildLocalReviewInstructions(request, this.config, diffContext);
218
222
  const toolContext = this.createLocalToolContext(sessionId, pseudoRequest, diffContext);
219
223
  this.setToolContext(toolContext);
220
- const aiResponse = await this.neurolink.generate({
224
+ const aiResponse = await this.generateWithRetry(() => this.neurolink.generate({
221
225
  input: { text: instructions },
222
226
  provider: this.config.ai.provider,
223
227
  model: this.config.ai.model,
224
228
  temperature: this.config.ai.temperature,
225
- maxTokens: Math.min(this.config.ai.maxTokens, 16_000),
229
+ maxTokens: clampMaxTokens(this.config.ai.maxTokens),
226
230
  timeout: this.config.ai.timeout,
227
231
  enableAnalytics: this.config.ai.enableAnalytics,
228
232
  enableEvaluation: this.config.ai.enableEvaluation,
@@ -242,7 +246,7 @@ export class YamaOrchestrator {
242
246
  headRef: diffContext.headRef,
243
247
  },
244
248
  },
245
- });
249
+ }), "local-review");
246
250
  this.recordToolCallsFromResponse(sessionId, aiResponse);
247
251
  const result = this.parseLocalReviewResult(aiResponse, sessionId, startTime, request, diffContext);
248
252
  // Stored as generic session payload for debugging/export parity.
@@ -254,61 +258,6 @@ export class YamaOrchestrator {
254
258
  throw error;
255
259
  }
256
260
  }
257
- /**
258
- * Stream review with real-time updates (for verbose mode)
259
- */
260
- async *streamReview(request) {
261
- await this.ensureInitialized("pr", request.configPath, request);
262
- const sessionId = this.sessionManager.createSession(request);
263
- try {
264
- const bootstrapStandards = await this.getBootstrappedStandards(request, sessionId);
265
- // Build instructions
266
- const instructions = await this.promptBuilder.buildReviewInstructions(request, this.config, bootstrapStandards, this.detectedProvider);
267
- // Create tool context
268
- const toolContext = this.createToolContext(sessionId, request);
269
- this.setToolContext(toolContext);
270
- // Stream AI execution
271
- yield {
272
- type: "progress",
273
- timestamp: new Date().toISOString(),
274
- sessionId,
275
- data: {
276
- phase: "context_gathering",
277
- progress: 0,
278
- message: "Starting review...",
279
- },
280
- };
281
- // Note: NeuroLink streaming implementation depends on version
282
- // This is a placeholder for streaming functionality
283
- const aiResponse = await this.neurolink.generate({
284
- input: { text: instructions },
285
- provider: this.config.ai.provider,
286
- model: this.config.ai.model,
287
- skipToolPromptInjection: true,
288
- ...this.getPRToolFilteringOptions(instructions),
289
- context: {
290
- sessionId,
291
- userId: this.getUserId(request),
292
- },
293
- memory: { enabled: false },
294
- enableAnalytics: true,
295
- });
296
- yield {
297
- type: "progress",
298
- timestamp: new Date().toISOString(),
299
- sessionId,
300
- data: {
301
- phase: "decision_making",
302
- progress: 100,
303
- message: "Review complete",
304
- },
305
- };
306
- }
307
- catch (error) {
308
- this.sessionManager.failSession(sessionId, error);
309
- throw error;
310
- }
311
- }
312
261
  /**
313
262
  * Start review and then enhance description in the same session
314
263
  * This allows the AI to use knowledge gained during review to write better descriptions
@@ -345,7 +294,7 @@ export class YamaOrchestrator {
345
294
  provider: this.config.ai.provider,
346
295
  model: this.config.ai.model,
347
296
  temperature: this.config.ai.temperature,
348
- maxTokens: this.config.ai.maxTokens,
297
+ maxTokens: clampMaxTokens(this.config.ai.maxTokens),
349
298
  timeout: this.config.ai.timeout,
350
299
  skipToolPromptInjection: true,
351
300
  ...this.getPRToolFilteringOptions(reviewInstructions),
@@ -378,7 +327,7 @@ export class YamaOrchestrator {
378
327
  provider: this.config.ai.provider,
379
328
  model: this.config.ai.model,
380
329
  temperature: this.config.ai.temperature,
381
- maxTokens: this.config.ai.maxTokens,
330
+ maxTokens: clampMaxTokens(this.config.ai.maxTokens),
382
331
  timeout: this.config.ai.timeout,
383
332
  skipToolPromptInjection: true,
384
333
  ...this.getPRToolFilteringOptions(enhanceInstructions),
@@ -494,7 +443,7 @@ export class YamaOrchestrator {
494
443
  branch: request.branch,
495
444
  dryRun: request.dryRun || false,
496
445
  metadata: {
497
- yamaVersion: "2.2.1",
446
+ yamaVersion: VERSION,
498
447
  startTime: new Date().toISOString(),
499
448
  },
500
449
  };
@@ -511,7 +460,7 @@ export class YamaOrchestrator {
511
460
  repository: request.repository || request.repo,
512
461
  dryRun: request.dryRun || false,
513
462
  metadata: {
514
- yamaVersion: "2.2.1",
463
+ yamaVersion: VERSION,
515
464
  startTime: new Date().toISOString(),
516
465
  repoPath: diffContext.repoPath,
517
466
  diffSource: diffContext.diffSource,
@@ -524,6 +473,70 @@ export class YamaOrchestrator {
524
473
  this.currentToolContext = context;
525
474
  this.neurolink.setToolContext(context);
526
475
  }
476
+ /**
477
+ * Run a NeuroLink generate() call with a small bounded retry on transient
478
+ * errors (wires up the previously-unused `ai.retryAttempts` config). Retries
479
+ * up to `retryAttempts` times (>=1 total attempt) with a short exponential
480
+ * backoff. Non-transient errors (e.g. auth/validation) fail fast on the first
481
+ * attempt so we don't paper over real misconfiguration.
482
+ */
483
+ async generateWithRetry(run, label) {
484
+ const configured = this.config.ai.retryAttempts;
485
+ const maxAttempts = typeof configured === "number" && Number.isFinite(configured)
486
+ ? Math.max(1, Math.floor(configured))
487
+ : 1;
488
+ let lastError;
489
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
490
+ try {
491
+ return await run();
492
+ }
493
+ catch (error) {
494
+ lastError = error;
495
+ if (attempt >= maxAttempts || !this.isTransientError(error)) {
496
+ throw error;
497
+ }
498
+ const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 8000);
499
+ console.warn(` ⚠️ ${label} generate attempt ${attempt}/${maxAttempts} failed ` +
500
+ `(${error.message}); retrying in ${backoffMs}ms...`);
501
+ await this.delay(backoffMs);
502
+ }
503
+ }
504
+ // Unreachable in practice (loop either returns or throws), but keeps types happy.
505
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
506
+ }
507
+ /**
508
+ * Heuristic transient-error classifier for retry decisions. Conservative:
509
+ * matches common network / rate-limit / timeout / 5xx signals and leaves
510
+ * everything else (auth, validation, 4xx) as non-retryable.
511
+ */
512
+ isTransientError(error) {
513
+ const message = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
514
+ if (!message) {
515
+ return false;
516
+ }
517
+ return (message.includes("timeout") ||
518
+ message.includes("timed out") ||
519
+ message.includes("etimedout") ||
520
+ message.includes("econnreset") ||
521
+ message.includes("econnrefused") ||
522
+ message.includes("enotfound") ||
523
+ message.includes("eai_again") ||
524
+ message.includes("socket hang up") ||
525
+ message.includes("network") ||
526
+ message.includes("rate limit") ||
527
+ message.includes("rate_limit") ||
528
+ message.includes("too many requests") ||
529
+ message.includes("429") ||
530
+ message.includes("500") ||
531
+ message.includes("502") ||
532
+ message.includes("503") ||
533
+ message.includes("504") ||
534
+ message.includes("overloaded") ||
535
+ message.includes("temporarily unavailable"));
536
+ }
537
+ delay(ms) {
538
+ return new Promise((resolve) => setTimeout(resolve, ms));
539
+ }
527
540
  /**
528
541
  * Parse AI response into structured review result
529
542
  */
@@ -544,11 +557,13 @@ export class YamaOrchestrator {
544
557
  summary: this.extractSummary(aiResponse),
545
558
  duration,
546
559
  tokenUsage: {
547
- input: this.toSafeNumber(aiResponse?.usage?.inputTokens),
548
- output: this.toSafeNumber(aiResponse?.usage?.outputTokens),
549
- total: this.toSafeNumber(aiResponse?.usage?.totalTokens),
560
+ // NeuroLink 9.70.x usage shape is { input, output, total } — NOT
561
+ // inputTokens/outputTokens/totalTokens (those always resolved to 0).
562
+ input: this.toSafeNumber(aiResponse?.usage?.input),
563
+ output: this.toSafeNumber(aiResponse?.usage?.output),
564
+ total: this.toSafeNumber(aiResponse?.usage?.total),
550
565
  },
551
- costEstimate: this.calculateCost(aiResponse.usage),
566
+ costEstimate: this.calculateCost(aiResponse),
552
567
  sessionId,
553
568
  };
554
569
  }
@@ -571,28 +586,52 @@ export class YamaOrchestrator {
571
586
  */
572
587
  parseLocalReviewResult(aiResponse, sessionId, startTime, request, diffContext) {
573
588
  const rawContent = aiResponse?.content || aiResponse?.outputs?.text || "";
574
- const parsed = this.extractJsonPayload(rawContent);
589
+ // Prefer NeuroLink's already-parsed structuredData (output.format "json")
590
+ // before re-parsing the raw content string by hand; fall back to the
591
+ // hand-parse so older shapes / non-JSON outputs still work.
592
+ const structured = aiResponse?.structuredData &&
593
+ typeof aiResponse.structuredData === "object" &&
594
+ !Array.isArray(aiResponse.structuredData)
595
+ ? aiResponse.structuredData
596
+ : null;
597
+ const parsed = structured ?? this.extractJsonPayload(rawContent);
598
+ const jsonTruncated = aiResponse?.jsonTruncated === true;
575
599
  const usage = aiResponse?.usage || {};
576
600
  const tokenUsage = {
577
- input: this.toSafeNumber(usage.inputTokens),
578
- output: this.toSafeNumber(usage.outputTokens),
579
- total: this.toSafeNumber(usage.totalTokens),
601
+ // NeuroLink 9.70.x usage shape is { input, output, total }.
602
+ input: this.toSafeNumber(usage.input),
603
+ output: this.toSafeNumber(usage.output),
604
+ total: this.toSafeNumber(usage.total),
580
605
  };
581
606
  if (!parsed) {
582
- const fallbackIssue = {
583
- id: "OUTPUT_FORMAT_VIOLATION",
584
- severity: "MAJOR",
585
- category: "review-engine",
586
- title: "Model did not return structured JSON",
587
- description: "Local review response was unstructured (likely tool-call trace or partial output), so findings cannot be trusted.",
588
- suggestion: "Retry with a tool-calling capable model or reduce review scope (smaller diff / includePaths) to keep responses structured.",
589
- };
607
+ // Distinguish a truncated response (model ran out of output budget mid-JSON)
608
+ // from a genuinely malformed one, and advise raising maxTokens in that case
609
+ // rather than emitting the generic format-violation guidance.
610
+ const fallbackIssue = jsonTruncated
611
+ ? {
612
+ id: "OUTPUT_TRUNCATED",
613
+ severity: "MAJOR",
614
+ category: "review-engine",
615
+ title: "Model output was truncated before valid JSON completed",
616
+ description: "The local review response was cut off (jsonTruncated), so the JSON could not be parsed and findings cannot be trusted.",
617
+ suggestion: "Raise ai.maxTokens (output budget) and/or reduce review scope (smaller diff / includePaths) so the structured JSON can finish.",
618
+ }
619
+ : {
620
+ id: "OUTPUT_FORMAT_VIOLATION",
621
+ severity: "MAJOR",
622
+ category: "review-engine",
623
+ title: "Model did not return structured JSON",
624
+ description: "Local review response was unstructured (likely tool-call trace or partial output), so findings cannot be trusted.",
625
+ suggestion: "Retry with a tool-calling capable model or reduce review scope (smaller diff / includePaths) to keep responses structured.",
626
+ };
590
627
  const issuesBySeverity = this.countFindingsBySeverity([fallbackIssue]);
591
628
  return {
592
629
  mode: "local",
593
630
  decision: "CHANGES_REQUESTED",
594
631
  summary: this.sanitizeLocalSummary(rawContent) ||
595
- "Local review could not produce structured JSON output.",
632
+ (jsonTruncated
633
+ ? "Local review output was truncated before valid JSON completed."
634
+ : "Local review could not produce structured JSON output."),
596
635
  issues: [fallbackIssue],
597
636
  enhancements: [],
598
637
  statistics: {
@@ -605,7 +644,7 @@ export class YamaOrchestrator {
605
644
  },
606
645
  duration: Math.round((Date.now() - startTime) / 1000),
607
646
  tokenUsage,
608
- costEstimate: this.calculateCost(aiResponse?.usage),
647
+ costEstimate: this.calculateCost(aiResponse),
609
648
  sessionId,
610
649
  schemaVersion: request.outputSchemaVersion || "1.0",
611
650
  metadata: {
@@ -642,7 +681,7 @@ export class YamaOrchestrator {
642
681
  },
643
682
  duration: Math.round((Date.now() - startTime) / 1000),
644
683
  tokenUsage,
645
- costEstimate: this.calculateCost(aiResponse?.usage),
684
+ costEstimate: this.calculateCost(aiResponse),
646
685
  sessionId,
647
686
  schemaVersion: request.outputSchemaVersion || "1.0",
648
687
  metadata: {
@@ -728,7 +767,11 @@ export class YamaOrchestrator {
728
767
  return findings
729
768
  .map((raw, index) => {
730
769
  const value = (raw || {});
731
- const severity = this.normalizeSeverity(value.severity);
770
+ const ruleKey = (typeof value.rule === "string" && value.rule) ||
771
+ (typeof value.category === "string" && value.category) ||
772
+ (typeof value.id === "string" && value.id) ||
773
+ undefined;
774
+ const severity = this.normalizeSeverity(value.severity, ruleKey);
732
775
  if (!severity) {
733
776
  return null;
734
777
  }
@@ -758,9 +801,28 @@ export class YamaOrchestrator {
758
801
  })
759
802
  .filter((item) => item !== null);
760
803
  }
761
- normalizeSeverity(severity) {
804
+ normalizeSeverity(severity, ruleKey) {
805
+ // Calibration: allow projectStandards.severityOverrides to remap a finding's
806
+ // severity by its rule/category key (defensive — config is optional and the
807
+ // override value is itself validated against the known severity set). This
808
+ // takes precedence over the model-reported severity.
809
+ const overrides = this.config.projectStandards?.severityOverrides;
810
+ if (overrides && ruleKey) {
811
+ const overridden = this.coerceSeverity(overrides[ruleKey]);
812
+ if (overridden) {
813
+ return overridden;
814
+ }
815
+ }
816
+ return this.coerceSeverity(severity) ?? "MINOR";
817
+ }
818
+ /**
819
+ * Map an arbitrary value to a known severity, or null if it is not one of the
820
+ * recognised levels. Unlike normalizeSeverity this does NOT default to MINOR,
821
+ * so callers can distinguish "unknown" from a real level.
822
+ */
823
+ coerceSeverity(severity) {
762
824
  if (typeof severity !== "string") {
763
- return "MINOR";
825
+ return null;
764
826
  }
765
827
  const value = severity.toUpperCase();
766
828
  if (value === "CRITICAL" ||
@@ -769,7 +831,7 @@ export class YamaOrchestrator {
769
831
  value === "SUGGESTION") {
770
832
  return value;
771
833
  }
772
- return "MINOR";
834
+ return null;
773
835
  }
774
836
  countFindingsBySeverity(findings) {
775
837
  const counts = {
@@ -821,24 +883,24 @@ export class YamaOrchestrator {
821
883
  // set_pr_approval + legacy names) byte-for-byte.
822
884
  const toolCalls = session.toolCalls || [];
823
885
  const ts = getProviderToolset(this.detectedProvider);
824
- let requestedChanges;
825
- let approved;
886
+ // ORDER-AWARE: the LAST decisive review-status / approval tool call wins, so
887
+ // a later approval correctly overrides an earlier request-changes (and vice
888
+ // versa). The previous logic let any "BLOCKED" stick regardless of order,
889
+ // reporting BLOCKED even when the AI changed its mind and approved afterward.
890
+ let lastDecision;
826
891
  for (const tc of toolCalls) {
827
892
  const decision = ts.interpretDecision({
828
893
  toolName: tc?.toolName,
829
894
  args: tc?.args || {},
830
895
  });
831
- if (decision === "BLOCKED") {
832
- requestedChanges = true;
833
- }
834
- else if (decision === "APPROVED") {
835
- approved = true;
896
+ if (decision === "BLOCKED" || decision === "APPROVED") {
897
+ lastDecision = decision;
836
898
  }
837
899
  }
838
- if (requestedChanges === true) {
900
+ if (lastDecision === "BLOCKED") {
839
901
  return "BLOCKED";
840
902
  }
841
- if (approved === true) {
903
+ if (lastDecision === "APPROVED") {
842
904
  return "APPROVED";
843
905
  }
844
906
  // Default to changes requested if unclear
@@ -925,7 +987,10 @@ export class YamaOrchestrator {
925
987
  suggestions: 0,
926
988
  };
927
989
  commentCalls.forEach((call) => {
928
- const text = call.args?.comment_text || "";
990
+ // Comment body field differs per provider: Bitbucket comment tools use
991
+ // `comment_text`, GitHub comment tools use `body`. Read either so severity
992
+ // counts work for both (GitHub previously always counted 0).
993
+ const text = call.args?.comment_text ?? call.args?.body ?? "";
929
994
  if (text.includes("🔒 CRITICAL") || text.includes("CRITICAL:")) {
930
995
  counts.critical++;
931
996
  }
@@ -949,17 +1014,31 @@ export class YamaOrchestrator {
949
1014
  return aiResponse.content || aiResponse.text || "Review completed";
950
1015
  }
951
1016
  /**
952
- * Calculate cost estimate from token usage
1017
+ * Calculate cost estimate from an AI response.
1018
+ *
1019
+ * Prefers NeuroLink's own analytics cost (`response.analytics.cost`, a USD
1020
+ * number computed per-provider/model when enableAnalytics is true — Yama sets
1021
+ * it). The previous implementation hardcoded Gemini-2.0-Flash pricing
1022
+ * ($0.25/$1.00 per 1M) for EVERY provider, which is ~12-15x too low for Claude.
1023
+ * Only when the analytics cost is unavailable do we fall back to a clearly
1024
+ * labeled rough estimate based on the { input, output } token usage.
953
1025
  */
954
- calculateCost(usage) {
1026
+ calculateCost(aiResponse) {
1027
+ const analyticsCost = aiResponse?.analytics?.cost;
1028
+ if (typeof analyticsCost === "number" && Number.isFinite(analyticsCost)) {
1029
+ return Number(analyticsCost.toFixed(6));
1030
+ }
1031
+ const usage = aiResponse?.usage;
955
1032
  if (!usage) {
956
1033
  return 0;
957
1034
  }
958
- // Rough estimates (update with actual pricing)
959
- const inputCostPer1M = 0.25; // $0.25 per 1M input tokens (Gemini 2.0 Flash)
960
- const outputCostPer1M = 1.0; // $1.00 per 1M output tokens
961
- const inputTokens = this.toSafeNumber(usage.inputTokens);
962
- const outputTokens = this.toSafeNumber(usage.outputTokens);
1035
+ // ROUGH FALLBACK ONLY provider/model agnostic placeholder pricing used
1036
+ // when NeuroLink analytics cost is unavailable. Not accurate for any
1037
+ // specific model; treat as an order-of-magnitude estimate.
1038
+ const inputCostPer1M = 0.25;
1039
+ const outputCostPer1M = 1.0;
1040
+ const inputTokens = this.toSafeNumber(usage.input);
1041
+ const outputTokens = this.toSafeNumber(usage.output);
963
1042
  const inputCost = (inputTokens / 1_000_000) * inputCostPer1M;
964
1043
  const outputCost = (outputTokens / 1_000_000) * outputCostPer1M;
965
1044
  const totalCost = inputCost + outputCost;
@@ -985,7 +1064,27 @@ export class YamaOrchestrator {
985
1064
  if (mode === "off") {
986
1065
  return {};
987
1066
  }
988
- const hasJiraSignal = /\b[A-Z]{2,}-\d+\b/.test(inputText) || /\bjira\b/i.test(inputText);
1067
+ // Jira issue-key signal. The naive /\b[A-Z]{2,}-\d+\b/ over-matched common
1068
+ // non-Jira tokens (UTF-8, SHA-256, SHA256-..., CVE-2024, ISO-8601, RFC-3339),
1069
+ // wrongly keeping Jira tools enabled. Exclude a small denylist of those
1070
+ // prefixes so the match stays conservative (still keeps tools when a real
1071
+ // ABC-123 style key — or the literal word "jira" — is present).
1072
+ const jiraKeyPattern = /\b([A-Z]{2,})-\d+\b/g;
1073
+ const nonJiraKeyPrefixes = new Set([
1074
+ "UTF",
1075
+ "SHA",
1076
+ "SHA256",
1077
+ "SHA512",
1078
+ "MD5",
1079
+ "CVE",
1080
+ "ISO",
1081
+ "RFC",
1082
+ "UTC",
1083
+ "GMT",
1084
+ "BASE64",
1085
+ ]);
1086
+ const hasJiraKey = Array.from(inputText.matchAll(jiraKeyPattern), (m) => m[1]).some((prefix) => !nonJiraKeyPrefixes.has(prefix));
1087
+ const hasJiraSignal = hasJiraKey || /\bjira\b/i.test(inputText);
989
1088
  if (hasJiraSignal) {
990
1089
  return {};
991
1090
  }
@@ -1013,7 +1112,11 @@ export class YamaOrchestrator {
1013
1112
  }
1014
1113
  /**
1015
1114
  * Query-level read-only filtering for local-git MCP tools.
1016
- * Uses regex-based mutation detection instead of hardcoded tool-name lists.
1115
+ *
1116
+ * Uses the shared, fail-closed `isMutatingGitTool` helper (any git_* tool not
1117
+ * on the read-only allow-list is treated as mutating) so the orchestrator,
1118
+ * explorer, and MCP manager all agree on what is read-only. The previous local
1119
+ * regex silently missed mutating tools like git_branch / git_mv / git_pull.
1017
1120
  */
1018
1121
  getLocalToolFilteringOptions() {
1019
1122
  try {
@@ -1021,15 +1124,17 @@ export class YamaOrchestrator {
1021
1124
  if (!Array.isArray(externalTools)) {
1022
1125
  return {};
1023
1126
  }
1024
- const mutatingGitToolPattern = /^git_(commit|push|add|checkout|create_branch|merge|rebase|cherry_pick|reset|revert|tag|rm|clean|stash|apply)\b/i;
1025
1127
  // High-volume read operations can flood context with huge payloads.
1026
1128
  const highVolumeGitToolPattern = /^git_(diff|diff_staged|diff_unstaged|log|show)\b/i;
1027
1129
  const excludeTools = externalTools
1028
1130
  .filter((tool) => tool?.serverId === "local-git")
1029
1131
  .map((tool) => tool?.name)
1030
1132
  .filter((name) => typeof name === "string")
1031
- .filter((name) => mutatingGitToolPattern.test(this.normalizeToolName(name)) ||
1032
- highVolumeGitToolPattern.test(this.normalizeToolName(name)));
1133
+ .filter((name) => {
1134
+ const normalized = this.normalizeToolName(name);
1135
+ return (isMutatingGitTool(normalized) ||
1136
+ highVolumeGitToolPattern.test(normalized));
1137
+ });
1033
1138
  if (excludeTools.length === 0) {
1034
1139
  return {};
1035
1140
  }
@@ -1239,7 +1344,7 @@ export class YamaOrchestrator {
1239
1344
  console.log(`
1240
1345
  ⚔️ YAMA - AI-Native Code Review Guardian
1241
1346
 
1242
- Version: 2.2.1
1347
+ Version: ${VERSION}
1243
1348
  Mode: Autonomous AI-Powered Review
1244
1349
  Powered by: NeuroLink + MCP Tools
1245
1350
  `);
@@ -6,6 +6,8 @@ import { MCPServerManager } from "../core/MCPServerManager.js";
6
6
  import { KnowledgeBaseManager } from "../learning/KnowledgeBaseManager.js";
7
7
  import { buildObservabilityConfigFromEnv, validateObservabilityConfig, } from "../utils/ObservabilityConfig.js";
8
8
  import { getProviderToolset, } from "../providers/ProviderToolset.js";
9
+ import { clampMaxTokens, MAX_EXTRACTION_TOKENS } from "../utils/tokenLimits.js";
10
+ import { isMutatingGitTool } from "../utils/toolPolicy.js";
9
11
  import { ExplorerPromptBuilder } from "./ExplorerPromptBuilder.js";
10
12
  import { RulesContextLoader } from "./RulesContextLoader.js";
11
13
  export class ContextExplorerService {
@@ -78,7 +80,7 @@ export class ContextExplorerService {
78
80
  provider: this.config.ai.explore.provider || this.config.ai.provider,
79
81
  model: this.config.ai.explore.model || this.config.ai.model,
80
82
  temperature: this.config.ai.explore.temperature ?? this.config.ai.temperature,
81
- maxTokens: this.config.ai.explore.maxTokens || this.config.ai.maxTokens,
83
+ maxTokens: clampMaxTokens(this.config.ai.explore.maxTokens ?? this.config.ai.maxTokens),
82
84
  timeout: this.config.ai.explore.timeout || this.config.ai.timeout,
83
85
  skipToolPromptInjection: true,
84
86
  ...this.getToolFilteringOptions(runtimeContext.mode),
@@ -98,7 +100,7 @@ export class ContextExplorerService {
98
100
  provider: this.config.ai.explore.provider || this.config.ai.provider,
99
101
  model: this.config.ai.explore.model || this.config.ai.model,
100
102
  temperature: 0.1,
101
- maxTokens: Math.min(this.config.ai.explore.maxTokens || this.config.ai.maxTokens, 12_000),
103
+ maxTokens: clampMaxTokens(this.config.ai.explore.maxTokens ?? this.config.ai.maxTokens, MAX_EXTRACTION_TOKENS),
102
104
  timeout: "2m",
103
105
  disableTools: true,
104
106
  context: {
@@ -202,11 +204,17 @@ export class ContextExplorerService {
202
204
  static MUTATION_TOOL_NAMES = new Set(["bitbucket", "github"].flatMap((provider) => getProviderToolset(provider).mutationToolNames.map((name) => name.toLowerCase())));
203
205
  shouldExcludeTool(mode, toolName) {
204
206
  const normalized = this.normalizeToolName(toolName);
207
+ // Provider (non-git) mutation tools — Bitbucket/GitHub write tools derived
208
+ // from each provider toolset — are always excluded for the read-only
209
+ // explore subagent.
205
210
  if (ContextExplorerService.MUTATION_TOOL_NAMES.has(normalized.toLowerCase())) {
206
211
  return true;
207
212
  }
213
+ // Git tools are filtered with the shared fail-closed allow-list in local
214
+ // mode (any git_* tool not on the read-only list is treated as mutating),
215
+ // closing the gaps the old regex missed (git_branch, git_mv, git_pull, …).
208
216
  if (mode === "local") {
209
- return /^git_(commit|push|add|checkout|create_branch|merge|rebase|cherry_pick|reset|revert|tag|rm|clean|stash|apply)\b/i.test(normalized);
217
+ return isMutatingGitTool(toolName);
210
218
  }
211
219
  return false;
212
220
  }
@@ -50,6 +50,7 @@ ${toolUsageSection}
50
50
  <level name="MAJOR" emoji="⚠️">Blocks if multiple. MUST include a real-code suggestion. Logic bugs, perf issues, broken APIs.</level>
51
51
  <level name="MINOR" emoji="💡">Request changes. Suggestion optional. Quality, naming, duplication.</level>
52
52
  <level name="SUGGESTION" emoji="💬">Informational. Optimizations and improvements.</level>
53
+ <marker-rule>EVERY posted inline comment body MUST BEGIN with its severity marker as the very first token, exactly one of: "🔒 CRITICAL:", "⚠️ MAJOR:", "💡 MINOR:", "💬 SUGGESTION:". Write the marker verbatim (emoji + level + colon) before any other text — it is how issues are counted, so an omitted or altered marker is not counted.</marker-rule>
53
54
  </severity-levels>
54
55
 
55
56
  <anti-patterns>
@@ -140,7 +140,9 @@ class BitbucketToolset {
140
140
  history-dependent behavior — <!-- EXPLORE_BEGIN -->call explore_context with a precise
141
141
  task and wait for its evidence before commenting<!-- EXPLORE_END --><!-- EXPLORE_DISABLED_BEGIN -->use search_code or get_file_content to verify before commenting<!-- EXPLORE_DISABLED_END -->.
142
142
  d. For every confirmed issue, call add_comment immediately with line_number and
143
- line_type from the diff JSON. Include a real-code suggestion for CRITICAL/MAJOR.
143
+ line_type from the diff JSON. Begin comment_text with the exact severity marker as
144
+ its first token — "🔒 CRITICAL:", "⚠️ MAJOR:", "💡 MINOR:", or "💬 SUGGESTION:" —
145
+ then the message. Include a real-code suggestion for CRITICAL/MAJOR.
144
146
  e. Move to the next file. Never request another file's diff before finishing the
145
147
  current one. Never request a multi-file diff.
146
148
 
@@ -330,8 +332,10 @@ class GitHubToolset {
330
332
  history-dependent behavior — <!-- EXPLORE_BEGIN -->call explore_context with a precise
331
333
  task and wait for its evidence before commenting<!-- EXPLORE_END --><!-- EXPLORE_DISABLED_BEGIN -->use search_code or get_file_contents to verify before commenting<!-- EXPLORE_DISABLED_END -->.
332
334
  d. For every confirmed issue, call add_comment_to_pending_review immediately with
333
- path, line, subjectType="LINE", and body taken from the diff. Include a real-code
334
- suggestion in body for CRITICAL/MAJOR.
335
+ path, line, subjectType="LINE", and body taken from the diff. Begin body with the
336
+ exact severity marker as its first token — "🔒 CRITICAL:", "⚠️ MAJOR:", "💡 MINOR:",
337
+ or "💬 SUGGESTION:" — then the message. Include a real-code suggestion in body for
338
+ CRITICAL/MAJOR.
335
339
  e. Move to the next file. Never jump between files mid-review.
336
340
 
337
341
  STEP 5 — Decision (submit the review)
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Centralized output-token limits for NeuroLink generate() calls.
3
+ *
4
+ * Yama previously passed `ai.maxTokens` (defaulting to 128000) straight to
5
+ * generate(). 128000 is a context-window size, not an output cap — no model
6
+ * emits that many output tokens, and on the non-streaming Vertex+Claude path it
7
+ * historically tripped the Anthropic SDK's "Streaming is required" guard
8
+ * (expected time > 10 min for maxTokens > ~21333). NeuroLink >= 9.70 clamps
9
+ * per-model server-side, but Yama still clamps to a sane ceiling so configs can
10
+ * never pass absurd values and behavior is consistent across every call site
11
+ * (previous clamps were inconsistent: 16_000 in one place, 12_000 in another,
12
+ * and unclamped in the main review path).
13
+ */
14
+ /** Sane default output-token ceiling for review/generation calls. */
15
+ export declare const MAX_OUTPUT_TOKENS = 32000;
16
+ /** Smaller ceiling for lightweight structured-extraction passes. */
17
+ export declare const MAX_EXTRACTION_TOKENS = 12000;
18
+ /**
19
+ * Clamp a requested maxTokens value to a safe ceiling.
20
+ *
21
+ * Returns `cap` when the requested value is missing, non-finite, or non-positive
22
+ * so callers always end up with a usable, bounded number.
23
+ */
24
+ export declare function clampMaxTokens(requested: number | undefined | null, cap?: number): number;
25
+ //# sourceMappingURL=tokenLimits.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Centralized output-token limits for NeuroLink generate() calls.
3
+ *
4
+ * Yama previously passed `ai.maxTokens` (defaulting to 128000) straight to
5
+ * generate(). 128000 is a context-window size, not an output cap — no model
6
+ * emits that many output tokens, and on the non-streaming Vertex+Claude path it
7
+ * historically tripped the Anthropic SDK's "Streaming is required" guard
8
+ * (expected time > 10 min for maxTokens > ~21333). NeuroLink >= 9.70 clamps
9
+ * per-model server-side, but Yama still clamps to a sane ceiling so configs can
10
+ * never pass absurd values and behavior is consistent across every call site
11
+ * (previous clamps were inconsistent: 16_000 in one place, 12_000 in another,
12
+ * and unclamped in the main review path).
13
+ */
14
+ /** Sane default output-token ceiling for review/generation calls. */
15
+ export const MAX_OUTPUT_TOKENS = 32_000;
16
+ /** Smaller ceiling for lightweight structured-extraction passes. */
17
+ export const MAX_EXTRACTION_TOKENS = 12_000;
18
+ /**
19
+ * Clamp a requested maxTokens value to a safe ceiling.
20
+ *
21
+ * Returns `cap` when the requested value is missing, non-finite, or non-positive
22
+ * so callers always end up with a usable, bounded number.
23
+ */
24
+ export function clampMaxTokens(requested, cap = MAX_OUTPUT_TOKENS) {
25
+ if (typeof requested !== "number" ||
26
+ !Number.isFinite(requested) ||
27
+ requested <= 0) {
28
+ return cap;
29
+ }
30
+ return Math.min(requested, cap);
31
+ }
32
+ //# sourceMappingURL=tokenLimits.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared tool-safety policy helpers.
3
+ *
4
+ * Centralizes git mutation detection so the orchestrator, the explorer, and the
5
+ * MCP server manager all agree on what is read-only. Uses a fail-closed
6
+ * allow-list: any `git_*` tool NOT on the read-only list is treated as mutating.
7
+ * The previous per-file blocklists silently missed mutating tools such as
8
+ * `git_branch`, `git_mv`, `git_pull`, `git_fetch`, `git_restore`, `git_switch`,
9
+ * and `git_remote`, leaving them exposed in "read-only" review modes.
10
+ */
11
+ /** Strip server/namespace prefixes, e.g. "local-git:git_status" -> "git_status". */
12
+ export declare function normalizeToolName(name: string): string;
13
+ /** Git MCP tools that only READ repository state and are always safe to expose. */
14
+ export declare const READ_ONLY_GIT_TOOLS: ReadonlySet<string>;
15
+ /**
16
+ * Returns true if the given tool is a git tool that can MUTATE repository state.
17
+ *
18
+ * Fail-closed: any `git_*` tool not on the read-only allow-list is treated as
19
+ * mutating. Non-git tools return false — callers handle provider-specific
20
+ * mutation tool lists (e.g. Bitbucket/GitHub write tools) separately.
21
+ */
22
+ export declare function isMutatingGitTool(toolName: string): boolean;
23
+ //# sourceMappingURL=toolPolicy.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared tool-safety policy helpers.
3
+ *
4
+ * Centralizes git mutation detection so the orchestrator, the explorer, and the
5
+ * MCP server manager all agree on what is read-only. Uses a fail-closed
6
+ * allow-list: any `git_*` tool NOT on the read-only list is treated as mutating.
7
+ * The previous per-file blocklists silently missed mutating tools such as
8
+ * `git_branch`, `git_mv`, `git_pull`, `git_fetch`, `git_restore`, `git_switch`,
9
+ * and `git_remote`, leaving them exposed in "read-only" review modes.
10
+ */
11
+ /** Strip server/namespace prefixes, e.g. "local-git:git_status" -> "git_status". */
12
+ export function normalizeToolName(name) {
13
+ return name.split(/[.:/]/).pop() || name;
14
+ }
15
+ /** Git MCP tools that only READ repository state and are always safe to expose. */
16
+ export const READ_ONLY_GIT_TOOLS = new Set([
17
+ "git_status",
18
+ "git_log",
19
+ "git_show",
20
+ "git_diff",
21
+ "git_diff_staged",
22
+ "git_diff_unstaged",
23
+ "git_blame",
24
+ ]);
25
+ /**
26
+ * Returns true if the given tool is a git tool that can MUTATE repository state.
27
+ *
28
+ * Fail-closed: any `git_*` tool not on the read-only allow-list is treated as
29
+ * mutating. Non-git tools return false — callers handle provider-specific
30
+ * mutation tool lists (e.g. Bitbucket/GitHub write tools) separately.
31
+ */
32
+ export function isMutatingGitTool(toolName) {
33
+ const name = normalizeToolName(toolName).toLowerCase();
34
+ if (!name.startsWith("git_")) {
35
+ return false;
36
+ }
37
+ return !READ_ONLY_GIT_TOOLS.has(name);
38
+ }
39
+ //# sourceMappingURL=toolPolicy.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/yama",
3
- "version": "2.6.0",
3
+ "version": "2.7.1",
4
4
  "description": "Enterprise-grade Pull Request automation toolkit with AI-powered code review and description enhancement",
5
5
  "keywords": [
6
6
  "pr",
@@ -90,7 +90,8 @@
90
90
  "check:all": "npm run lint && npm run format --check && npm run validate && npm run validate:commit"
91
91
  },
92
92
  "dependencies": {
93
- "@juspay/neurolink": "^9.42.0",
93
+ "@juspay/neurolink": "^9.70.7",
94
+ "@juspay/hippocampus": "^0.1.7",
94
95
  "langfuse": "^3.35.0",
95
96
  "@nexus2520/bitbucket-mcp-server": "2.0.4",
96
97
  "@nexus2520/jira-mcp-server": "^1.1.1",