@j0hanz/code-review-analyst-mcp 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.js +12 -3
  2. package/dist/instructions.md +4 -146
  3. package/dist/lib/context-budget.d.ts +2 -2
  4. package/dist/lib/context-budget.js +12 -6
  5. package/dist/lib/diff-budget.js +6 -2
  6. package/dist/lib/diff-parser.js +31 -36
  7. package/dist/lib/env-config.d.ts +1 -0
  8. package/dist/lib/env-config.js +9 -3
  9. package/dist/lib/errors.d.ts +1 -0
  10. package/dist/lib/errors.js +5 -5
  11. package/dist/lib/gemini-schema.js +2 -1
  12. package/dist/lib/gemini.js +135 -67
  13. package/dist/lib/model-config.d.ts +14 -2
  14. package/dist/lib/model-config.js +30 -6
  15. package/dist/lib/tool-contracts.d.ts +245 -0
  16. package/dist/lib/tool-contracts.js +302 -0
  17. package/dist/lib/tool-factory.d.ts +5 -1
  18. package/dist/lib/tool-factory.js +48 -54
  19. package/dist/lib/tool-response.js +10 -12
  20. package/dist/lib/types.d.ts +3 -3
  21. package/dist/prompts/index.js +47 -41
  22. package/dist/resources/index.d.ts +1 -1
  23. package/dist/resources/index.js +80 -18
  24. package/dist/resources/instructions.d.ts +1 -0
  25. package/dist/resources/instructions.js +59 -0
  26. package/dist/resources/server-config.d.ts +1 -0
  27. package/dist/resources/server-config.js +70 -0
  28. package/dist/resources/tool-catalog.d.ts +1 -0
  29. package/dist/resources/tool-catalog.js +39 -0
  30. package/dist/resources/tool-info.d.ts +5 -0
  31. package/dist/resources/tool-info.js +122 -0
  32. package/dist/resources/workflows.d.ts +1 -0
  33. package/dist/resources/workflows.js +72 -0
  34. package/dist/schemas/inputs.d.ts +8 -0
  35. package/dist/schemas/inputs.js +20 -18
  36. package/dist/schemas/outputs.d.ts +17 -1
  37. package/dist/schemas/outputs.js +84 -52
  38. package/dist/server.js +25 -26
  39. package/dist/tools/analyze-complexity.d.ts +2 -0
  40. package/dist/tools/analyze-complexity.js +45 -0
  41. package/dist/tools/analyze-pr-impact.js +30 -25
  42. package/dist/tools/detect-api-breaking.d.ts +2 -0
  43. package/dist/tools/detect-api-breaking.js +42 -0
  44. package/dist/tools/generate-review-summary.js +26 -20
  45. package/dist/tools/generate-test-plan.js +34 -28
  46. package/dist/tools/index.js +9 -2
  47. package/dist/tools/inspect-code-quality.js +46 -40
  48. package/dist/tools/suggest-search-replace.js +34 -27
  49. package/package.json +1 -1
@@ -9,22 +9,31 @@ import { createCachedEnvInt } from './env-config.js';
9
9
  import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
10
10
  // Lazy-cached: first call happens after parseCommandLineArgs() sets GEMINI_MODEL.
11
11
  let _defaultModel;
12
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
13
+ const GEMINI_MODEL_ENV_VAR = 'GEMINI_MODEL';
14
+ const GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR = 'GEMINI_HARM_BLOCK_THRESHOLD';
15
+ const GEMINI_INCLUDE_THOUGHTS_ENV_VAR = 'GEMINI_INCLUDE_THOUGHTS';
16
+ const GEMINI_API_KEY_ENV_VAR = 'GEMINI_API_KEY';
17
+ const GOOGLE_API_KEY_ENV_VAR = 'GOOGLE_API_KEY';
12
18
  function getDefaultModel() {
13
19
  if (_defaultModel !== undefined)
14
20
  return _defaultModel;
15
- const value = process.env.GEMINI_MODEL ?? 'gemini-2.5-flash';
21
+ const value = process.env[GEMINI_MODEL_ENV_VAR] ?? DEFAULT_MODEL;
16
22
  _defaultModel = value;
17
23
  return value;
18
24
  }
19
- const DEFAULT_MAX_RETRIES = 1;
20
- const DEFAULT_TIMEOUT_MS = 60_000;
25
+ const DEFAULT_MAX_RETRIES = 3;
26
+ const DEFAULT_TIMEOUT_MS = 90_000;
21
27
  const DEFAULT_MAX_OUTPUT_TOKENS = 16_384;
22
28
  const RETRY_DELAY_BASE_MS = 300;
23
29
  const RETRY_DELAY_MAX_MS = 5_000;
24
30
  const RETRY_JITTER_RATIO = 0.2;
25
31
  const DEFAULT_SAFETY_THRESHOLD = HarmBlockThreshold.BLOCK_NONE;
32
+ const DEFAULT_INCLUDE_THOUGHTS = false;
26
33
  const UNKNOWN_REQUEST_CONTEXT_VALUE = 'unknown';
27
34
  const RETRYABLE_NUMERIC_CODES = new Set([429, 500, 502, 503, 504]);
35
+ const DIGITS_ONLY_PATTERN = /^\d+$/;
36
+ const SLEEP_UNREF_OPTIONS = { ref: false };
28
37
  const maxConcurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', 10);
29
38
  const concurrencyWaitMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', 2_000);
30
39
  const concurrencyPollMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_POLL_MS', 25);
@@ -54,9 +63,11 @@ const SAFETY_THRESHOLD_BY_NAME = {
54
63
  };
55
64
  let cachedSafetyThresholdEnv;
56
65
  let cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
66
+ let cachedIncludeThoughtsEnv;
67
+ let cachedIncludeThoughts = DEFAULT_INCLUDE_THOUGHTS;
57
68
  const safetySettingsCache = new Map();
58
69
  function getSafetyThreshold() {
59
- const threshold = process.env.GEMINI_HARM_BLOCK_THRESHOLD;
70
+ const threshold = process.env[GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR];
60
71
  if (threshold === cachedSafetyThresholdEnv) {
61
72
  return cachedSafetyThreshold;
62
73
  }
@@ -65,30 +76,70 @@ function getSafetyThreshold() {
65
76
  cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
66
77
  return cachedSafetyThreshold;
67
78
  }
68
- const normalizedThreshold = threshold.trim().toUpperCase();
69
- if (normalizedThreshold in SAFETY_THRESHOLD_BY_NAME) {
70
- cachedSafetyThreshold =
71
- SAFETY_THRESHOLD_BY_NAME[normalizedThreshold];
79
+ const parsedThreshold = parseSafetyThreshold(threshold);
80
+ if (parsedThreshold) {
81
+ cachedSafetyThreshold = parsedThreshold;
72
82
  return cachedSafetyThreshold;
73
83
  }
74
84
  cachedSafetyThreshold = DEFAULT_SAFETY_THRESHOLD;
75
85
  return cachedSafetyThreshold;
76
86
  }
77
- function getThinkingConfig(thinkingBudget) {
78
- return thinkingBudget !== undefined
79
- ? { includeThoughts: true, thinkingBudget }
80
- : undefined;
87
+ function parseSafetyThreshold(threshold) {
88
+ const normalizedThreshold = threshold.trim().toUpperCase();
89
+ if (!(normalizedThreshold in SAFETY_THRESHOLD_BY_NAME)) {
90
+ return undefined;
91
+ }
92
+ return SAFETY_THRESHOLD_BY_NAME[normalizedThreshold];
93
+ }
94
+ function getThinkingConfig(thinkingBudget, includeThoughts) {
95
+ if (thinkingBudget === undefined) {
96
+ return undefined;
97
+ }
98
+ if (includeThoughts) {
99
+ return { includeThoughts: true, thinkingBudget };
100
+ }
101
+ return { thinkingBudget };
102
+ }
103
+ function parseBooleanEnv(value) {
104
+ const normalized = value.trim().toLowerCase();
105
+ if (normalized.length === 0) {
106
+ return undefined;
107
+ }
108
+ if (normalized === '1' ||
109
+ normalized === 'true' ||
110
+ normalized === 'yes' ||
111
+ normalized === 'on') {
112
+ return true;
113
+ }
114
+ if (normalized === '0' ||
115
+ normalized === 'false' ||
116
+ normalized === 'no' ||
117
+ normalized === 'off') {
118
+ return false;
119
+ }
120
+ return undefined;
121
+ }
122
+ function getDefaultIncludeThoughts() {
123
+ const value = process.env[GEMINI_INCLUDE_THOUGHTS_ENV_VAR];
124
+ if (value === cachedIncludeThoughtsEnv) {
125
+ return cachedIncludeThoughts;
126
+ }
127
+ cachedIncludeThoughtsEnv = value;
128
+ if (!value) {
129
+ cachedIncludeThoughts = DEFAULT_INCLUDE_THOUGHTS;
130
+ return cachedIncludeThoughts;
131
+ }
132
+ cachedIncludeThoughts = parseBooleanEnv(value) ?? DEFAULT_INCLUDE_THOUGHTS;
133
+ return cachedIncludeThoughts;
81
134
  }
82
135
  function getSafetySettings(threshold) {
83
136
  const cached = safetySettingsCache.get(threshold);
84
137
  if (cached) {
85
138
  return cached;
86
139
  }
87
- const settings = new Array(SAFETY_CATEGORIES.length);
88
- let index = 0;
140
+ const settings = new Array();
89
141
  for (const category of SAFETY_CATEGORIES) {
90
- settings[index] = { category, threshold };
91
- index += 1;
142
+ settings.push({ category, threshold });
92
143
  }
93
144
  safetySettingsCache.set(threshold, settings);
94
145
  return settings;
@@ -118,9 +169,9 @@ export function getCurrentRequestId() {
118
169
  return context?.requestId ?? UNKNOWN_REQUEST_CONTEXT_VALUE;
119
170
  }
120
171
  function getApiKey() {
121
- const apiKey = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
172
+ const apiKey = process.env[GEMINI_API_KEY_ENV_VAR] ?? process.env[GOOGLE_API_KEY_ENV_VAR];
122
173
  if (!apiKey) {
123
- throw new Error('Missing GEMINI_API_KEY or GOOGLE_API_KEY.');
174
+ throw new Error(`Missing ${GEMINI_API_KEY_ENV_VAR} or ${GOOGLE_API_KEY_ENV_VAR}.`);
124
175
  }
125
176
  return apiKey;
126
177
  }
@@ -183,7 +234,7 @@ function toNumericCode(candidate) {
183
234
  if (typeof candidate === 'number' && Number.isFinite(candidate)) {
184
235
  return candidate;
185
236
  }
186
- if (typeof candidate === 'string' && /^\d+$/.test(candidate)) {
237
+ if (typeof candidate === 'string' && DIGITS_ONLY_PATTERN.test(candidate)) {
187
238
  return Number.parseInt(candidate, 10);
188
239
  }
189
240
  return undefined;
@@ -195,43 +246,37 @@ function toUpperStringCode(candidate) {
195
246
  const normalized = candidate.trim().toUpperCase();
196
247
  return normalized.length > 0 ? normalized : undefined;
197
248
  }
249
+ function findFirstNumericCode(record, keys) {
250
+ for (const key of keys) {
251
+ const numericCode = toNumericCode(record[key]);
252
+ if (numericCode !== undefined) {
253
+ return numericCode;
254
+ }
255
+ }
256
+ return undefined;
257
+ }
258
+ function findFirstStringCode(record, keys) {
259
+ for (const key of keys) {
260
+ const stringCode = toUpperStringCode(record[key]);
261
+ if (stringCode !== undefined) {
262
+ return stringCode;
263
+ }
264
+ }
265
+ return undefined;
266
+ }
198
267
  function getNumericErrorCode(error) {
199
268
  const record = getNestedError(error);
200
269
  if (!record) {
201
270
  return undefined;
202
271
  }
203
- const fromStatus = toNumericCode(record.status);
204
- if (fromStatus !== undefined) {
205
- return fromStatus;
206
- }
207
- const fromStatusCode = toNumericCode(record.statusCode);
208
- if (fromStatusCode !== undefined) {
209
- return fromStatusCode;
210
- }
211
- const fromCode = toNumericCode(record.code);
212
- if (fromCode !== undefined) {
213
- return fromCode;
214
- }
215
- return undefined;
272
+ return findFirstNumericCode(record, ['status', 'statusCode', 'code']);
216
273
  }
217
274
  function getTransientErrorCode(error) {
218
275
  const record = getNestedError(error);
219
276
  if (!record) {
220
277
  return undefined;
221
278
  }
222
- const fromCode = toUpperStringCode(record.code);
223
- if (fromCode !== undefined) {
224
- return fromCode;
225
- }
226
- const fromStatus = toUpperStringCode(record.status);
227
- if (fromStatus !== undefined) {
228
- return fromStatus;
229
- }
230
- const fromStatusText = toUpperStringCode(record.statusText);
231
- if (fromStatusText !== undefined) {
232
- return fromStatusText;
233
- }
234
- return undefined;
279
+ return findFirstStringCode(record, ['code', 'status', 'statusText']);
235
280
  }
236
281
  function shouldRetry(error) {
237
282
  const numericCode = getNumericErrorCode(error);
@@ -254,21 +299,25 @@ function getRetryDelayMs(attempt) {
254
299
  return Math.min(RETRY_DELAY_MAX_MS, boundedDelay + jitter);
255
300
  }
256
301
  function buildGenerationConfig(request, abortSignal) {
257
- const systemInstruction = request.systemInstruction
258
- ? { systemInstruction: request.systemInstruction }
259
- : undefined;
260
- const thinkingConfig = getThinkingConfig(request.thinkingBudget);
261
- const safetySettings = getSafetySettings(getSafetyThreshold());
262
- return {
302
+ const includeThoughts = request.includeThoughts ?? getDefaultIncludeThoughts();
303
+ const thinkingConfig = getThinkingConfig(request.thinkingBudget, includeThoughts);
304
+ const config = {
263
305
  temperature: request.temperature ?? 0.2,
264
306
  maxOutputTokens: request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
265
307
  responseMimeType: 'application/json',
266
308
  responseSchema: request.responseSchema,
267
- ...(systemInstruction ?? undefined),
268
- ...(thinkingConfig ? { thinkingConfig } : undefined),
269
- safetySettings,
309
+ safetySettings: getSafetySettings(getSafetyThreshold()),
310
+ topP: 0.95,
311
+ topK: 40,
270
312
  abortSignal,
271
313
  };
314
+ if (request.systemInstruction) {
315
+ config.systemInstruction = request.systemInstruction;
316
+ }
317
+ if (thinkingConfig) {
318
+ config.thinkingConfig = thinkingConfig;
319
+ }
320
+ return config;
272
321
  }
273
322
  function combineSignals(signal, requestSignal) {
274
323
  return requestSignal ? AbortSignal.any([signal, requestSignal]) : signal;
@@ -284,6 +333,12 @@ function parseStructuredResponse(responseText) {
284
333
  throw new Error(`Model produced invalid JSON: ${getErrorMessage(error)}`);
285
334
  }
286
335
  }
336
+ function formatTimeoutErrorMessage(timeoutMs) {
337
+ return `Gemini request timed out after ${formatNumber(timeoutMs)}ms.`;
338
+ }
339
+ function formatConcurrencyLimitErrorMessage(limit, waitLimitMs) {
340
+ return `Too many concurrent Gemini calls (limit: ${formatNumber(limit)}; waited ${formatNumber(waitLimitMs)}ms).`;
341
+ }
287
342
  async function generateContentWithTimeout(request, model, timeoutMs) {
288
343
  const controller = new AbortController();
289
344
  const timeout = setTimeout(() => {
@@ -303,7 +358,7 @@ async function generateContentWithTimeout(request, model, timeoutMs) {
303
358
  throw new Error('Gemini request was cancelled.');
304
359
  }
305
360
  if (controller.signal.aborted) {
306
- throw new Error(`Gemini request timed out after ${formatNumber(timeoutMs)}ms.`, { cause: error });
361
+ throw new Error(formatTimeoutErrorMessage(timeoutMs), { cause: error });
307
362
  }
308
363
  throw error;
309
364
  }
@@ -325,7 +380,7 @@ async function executeAttempt(request, model, timeoutMs, attempt, onLog) {
325
380
  });
326
381
  return parseStructuredResponse(response.text);
327
382
  }
328
- async function waitBeforeRetry(attempt, error, onLog) {
383
+ async function waitBeforeRetry(attempt, error, onLog, requestSignal) {
329
384
  const delayMs = getRetryDelayMs(attempt);
330
385
  const reason = getErrorMessage(error);
331
386
  await emitGeminiLog(onLog, 'warning', {
@@ -336,7 +391,20 @@ async function waitBeforeRetry(attempt, error, onLog) {
336
391
  reason,
337
392
  },
338
393
  });
339
- await sleep(delayMs, undefined, { ref: false });
394
+ if (requestSignal?.aborted) {
395
+ throw new Error('Gemini request was cancelled.');
396
+ }
397
+ try {
398
+ await sleep(delayMs, undefined, requestSignal
399
+ ? { ...SLEEP_UNREF_OPTIONS, signal: requestSignal }
400
+ : SLEEP_UNREF_OPTIONS);
401
+ }
402
+ catch (sleepError) {
403
+ if (requestSignal?.aborted) {
404
+ throw new Error('Gemini request was cancelled.');
405
+ }
406
+ throw sleepError;
407
+ }
340
408
  }
341
409
  async function throwGeminiFailure(maxRetries, lastError, onLog) {
342
410
  const attempts = maxRetries + 1;
@@ -358,27 +426,29 @@ async function runWithRetries(request, model, timeoutMs, maxRetries, onLog) {
358
426
  }
359
427
  catch (error) {
360
428
  lastError = error;
361
- if (attempt >= maxRetries || !shouldRetry(error)) {
429
+ if (!canRetryAttempt(attempt, maxRetries, error)) {
362
430
  break;
363
431
  }
364
- await waitBeforeRetry(attempt, error, onLog);
432
+ await waitBeforeRetry(attempt, error, onLog, request.signal);
365
433
  }
366
434
  }
367
435
  return throwGeminiFailure(maxRetries, lastError, onLog);
368
436
  }
437
+ function canRetryAttempt(attempt, maxRetries, error) {
438
+ return attempt < maxRetries && shouldRetry(error);
439
+ }
369
440
  async function waitForConcurrencySlot(limit, requestSignal) {
370
441
  const waitLimitMs = concurrencyWaitMsConfig.get();
371
442
  const pollMs = concurrencyPollMsConfig.get();
372
- const startedAt = performance.now();
443
+ const deadline = performance.now() + waitLimitMs;
373
444
  while (activeCalls >= limit) {
374
445
  if (requestSignal?.aborted) {
375
446
  throw new Error('Gemini request was cancelled.');
376
447
  }
377
- const elapsedMs = performance.now() - startedAt;
378
- if (elapsedMs >= waitLimitMs) {
379
- throw new Error(`Too many concurrent Gemini calls (limit: ${formatNumber(limit)}; waited ${formatNumber(waitLimitMs)}ms).`);
448
+ if (performance.now() >= deadline) {
449
+ throw new Error(formatConcurrencyLimitErrorMessage(limit, waitLimitMs));
380
450
  }
381
- await sleep(pollMs, undefined, { ref: false });
451
+ await sleep(pollMs, undefined, SLEEP_UNREF_OPTIONS);
382
452
  }
383
453
  }
384
454
  export async function generateStructuredJson(request) {
@@ -390,9 +460,7 @@ export async function generateStructuredJson(request) {
390
460
  await waitForConcurrencySlot(limit, request.signal);
391
461
  activeCalls += 1;
392
462
  try {
393
- return await geminiContext.run({ requestId: nextRequestId(), model }, async () => {
394
- return runWithRetries(request, model, timeoutMs, maxRetries, onLog);
395
- });
463
+ return await geminiContext.run({ requestId: nextRequestId(), model }, () => runWithRetries(request, model, timeoutMs, maxRetries, onLog));
396
464
  }
397
465
  finally {
398
466
  activeCalls -= 1;
@@ -3,9 +3,21 @@ export declare const FLASH_MODEL = "gemini-2.5-flash";
3
3
  /** High-capability model for deep reasoning, quality inspection, and reliable code generation. */
4
4
  export declare const PRO_MODEL = "gemini-2.5-pro";
5
5
  /** Thinking budget (tokens) for Flash model thinking tasks (test plans, search/replace). */
6
- export declare const FLASH_THINKING_BUDGET = 8192;
6
+ export declare const FLASH_THINKING_BUDGET: 8192;
7
7
  /** Thinking budget (tokens) for Pro model deep-analysis tasks (code quality inspection). */
8
- export declare const PRO_THINKING_BUDGET = 16384;
8
+ export declare const PRO_THINKING_BUDGET: 16384;
9
+ /** Output cap for Flash triage tools (impact, summary). */
10
+ export declare const FLASH_TRIAGE_MAX_OUTPUT_TOKENS: 2048;
11
+ /** Output cap for API breaking-change detection (migration guidance needs room). */
12
+ export declare const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS: 4096;
13
+ /** Output cap for test-plan generation (includes pseudocode snippets). */
14
+ export declare const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS: 4096;
15
+ /** Output cap for Pro deep review findings. */
16
+ export declare const PRO_REVIEW_MAX_OUTPUT_TOKENS: 8192;
17
+ /** Output cap for Pro search/replace remediation blocks. */
18
+ export declare const PRO_PATCH_MAX_OUTPUT_TOKENS: 4096;
19
+ /** Output cap for Flash complexity analysis reports. */
20
+ export declare const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS: 2048;
9
21
  /** Extended timeout for Pro model calls (ms). Pro thinks longer than Flash. */
10
22
  export declare const DEFAULT_TIMEOUT_PRO_MS = 120000;
11
23
  export declare const MODEL_TIMEOUT_MS: {
@@ -2,18 +2,42 @@
2
2
  export const FLASH_MODEL = 'gemini-2.5-flash';
3
3
  /** High-capability model for deep reasoning, quality inspection, and reliable code generation. */
4
4
  export const PRO_MODEL = 'gemini-2.5-pro';
5
- const FLASH_THINKING_BUDGET_VALUE = 8_192;
6
- const PRO_THINKING_BUDGET_VALUE = 16_384;
5
+ const THINKING_BUDGET_TOKENS = {
6
+ flash: 8_192,
7
+ pro: 16_384,
8
+ };
9
+ const OUTPUT_TOKEN_BUDGET = {
10
+ flashTriage: 2_048,
11
+ flashTestPlan: 4_096,
12
+ flashApiBreaking: 4_096,
13
+ flashComplexity: 2_048,
14
+ proReview: 8_192,
15
+ proPatch: 4_096,
16
+ };
17
+ const DEFAULT_DETECT_HINT = 'detect';
7
18
  /** Thinking budget (tokens) for Flash model thinking tasks (test plans, search/replace). */
8
- export const FLASH_THINKING_BUDGET = FLASH_THINKING_BUDGET_VALUE;
19
+ export const FLASH_THINKING_BUDGET = THINKING_BUDGET_TOKENS.flash;
9
20
  /** Thinking budget (tokens) for Pro model deep-analysis tasks (code quality inspection). */
10
- export const PRO_THINKING_BUDGET = PRO_THINKING_BUDGET_VALUE;
21
+ export const PRO_THINKING_BUDGET = THINKING_BUDGET_TOKENS.pro;
22
+ /** Output cap for Flash triage tools (impact, summary). */
23
+ export const FLASH_TRIAGE_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.flashTriage;
24
+ /** Output cap for API breaking-change detection (migration guidance needs room). */
25
+ export const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.flashApiBreaking;
26
+ /** Output cap for test-plan generation (includes pseudocode snippets). */
27
+ export const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.flashTestPlan;
28
+ /** Output cap for Pro deep review findings. */
29
+ export const PRO_REVIEW_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.proReview;
30
+ /** Output cap for Pro search/replace remediation blocks. */
31
+ export const PRO_PATCH_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.proPatch;
32
+ /** Output cap for Flash complexity analysis reports. */
33
+ export const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS = OUTPUT_TOKEN_BUDGET.flashComplexity;
11
34
  /** Extended timeout for Pro model calls (ms). Pro thinks longer than Flash. */
12
35
  export const DEFAULT_TIMEOUT_PRO_MS = 120_000;
13
36
  export const MODEL_TIMEOUT_MS = {
14
37
  defaultPro: DEFAULT_TIMEOUT_PRO_MS,
15
38
  };
39
+ Object.freeze(MODEL_TIMEOUT_MS);
16
40
  /** Default language hint when not specified by the user. Tells the model to auto-detect. */
17
- export const DEFAULT_LANGUAGE = 'detect';
41
+ export const DEFAULT_LANGUAGE = DEFAULT_DETECT_HINT;
18
42
  /** Default test-framework hint when not specified by the user. Tells the model to auto-detect. */
19
- export const DEFAULT_FRAMEWORK = 'detect';
43
+ export const DEFAULT_FRAMEWORK = DEFAULT_DETECT_HINT;
@@ -0,0 +1,245 @@
1
+ export interface ToolParameterContract {
2
+ name: string;
3
+ type: string;
4
+ required: boolean;
5
+ constraints: string;
6
+ description: string;
7
+ }
8
+ export interface ToolContract {
9
+ name: string;
10
+ purpose: string;
11
+ model: string;
12
+ timeoutMs: number;
13
+ thinkingBudget?: number;
14
+ maxOutputTokens: number;
15
+ params: readonly ToolParameterContract[];
16
+ outputShape: string;
17
+ gotchas: readonly string[];
18
+ crossToolFlow: readonly string[];
19
+ constraints?: readonly string[];
20
+ }
21
+ export declare const TOOL_CONTRACTS: readonly [{
22
+ readonly name: "analyze_pr_impact";
23
+ readonly purpose: "Assess severity, categories, breaking changes, and rollback complexity.";
24
+ readonly model: "gemini-2.5-flash";
25
+ readonly timeoutMs: 90000;
26
+ readonly maxOutputTokens: 2048;
27
+ readonly params: readonly [{
28
+ readonly name: "diff";
29
+ readonly type: "string";
30
+ readonly required: true;
31
+ readonly constraints: "10-120K chars";
32
+ readonly description: "Unified diff text.";
33
+ }, {
34
+ readonly name: "repository";
35
+ readonly type: "string";
36
+ readonly required: true;
37
+ readonly constraints: "1-200 chars";
38
+ readonly description: "Repository identifier (org/repo).";
39
+ }, {
40
+ readonly name: "language";
41
+ readonly type: "string";
42
+ readonly required: false;
43
+ readonly constraints: "2-32 chars";
44
+ readonly description: "Primary language hint.";
45
+ }];
46
+ readonly outputShape: "{severity, categories[], summary, breakingChanges[], affectedAreas[], rollbackComplexity}";
47
+ readonly gotchas: readonly ["Flash triage tool optimized for speed.", "Diff-only analysis (no full-file context)."];
48
+ readonly crossToolFlow: readonly ["severity/categories feed triage and merge-gate decisions."];
49
+ }, {
50
+ readonly name: "generate_review_summary";
51
+ readonly purpose: "Produce PR summary, risk rating, and merge recommendation.";
52
+ readonly model: "gemini-2.5-flash";
53
+ readonly timeoutMs: 90000;
54
+ readonly maxOutputTokens: 2048;
55
+ readonly params: readonly [{
56
+ readonly name: "diff";
57
+ readonly type: "string";
58
+ readonly required: true;
59
+ readonly constraints: "10-120K chars";
60
+ readonly description: "Unified diff text.";
61
+ }, {
62
+ readonly name: "repository";
63
+ readonly type: "string";
64
+ readonly required: true;
65
+ readonly constraints: "1-200 chars";
66
+ readonly description: "Repository identifier (org/repo).";
67
+ }, {
68
+ readonly name: "language";
69
+ readonly type: "string";
70
+ readonly required: false;
71
+ readonly constraints: "2-32 chars";
72
+ readonly description: "Primary language hint.";
73
+ }];
74
+ readonly outputShape: "{summary, overallRisk, keyChanges[], recommendation, stats{filesChanged, linesAdded, linesRemoved}}";
75
+ readonly gotchas: readonly ["stats are computed locally from the diff.", "Flash triage tool optimized for speed."];
76
+ readonly crossToolFlow: readonly ["Use before deep review to decide whether Pro analysis is needed."];
77
+ }, {
78
+ readonly name: "inspect_code_quality";
79
+ readonly purpose: "Deep code review with optional full-file context.";
80
+ readonly model: "gemini-2.5-pro";
81
+ readonly timeoutMs: 120000;
82
+ readonly thinkingBudget: 16384;
83
+ readonly maxOutputTokens: 8192;
84
+ readonly params: readonly [{
85
+ readonly name: "diff";
86
+ readonly type: "string";
87
+ readonly required: true;
88
+ readonly constraints: "10-120K chars";
89
+ readonly description: "Unified diff text.";
90
+ }, {
91
+ readonly name: "repository";
92
+ readonly type: "string";
93
+ readonly required: true;
94
+ readonly constraints: "1-200 chars";
95
+ readonly description: "Repository identifier (org/repo).";
96
+ }, {
97
+ readonly name: "language";
98
+ readonly type: "string";
99
+ readonly required: false;
100
+ readonly constraints: "2-32 chars";
101
+ readonly description: "Primary language hint.";
102
+ }, {
103
+ readonly name: "focusAreas";
104
+ readonly type: "string[]";
105
+ readonly required: false;
106
+ readonly constraints: "1-12 items, 2-80 chars each";
107
+ readonly description: "Focused inspection categories.";
108
+ }, {
109
+ readonly name: "maxFindings";
110
+ readonly type: "number";
111
+ readonly required: false;
112
+ readonly constraints: "1-25";
113
+ readonly description: "Post-generation cap applied to findings.";
114
+ }, {
115
+ readonly name: "files";
116
+ readonly type: "object[]";
117
+ readonly required: false;
118
+ readonly constraints: "1-20 files, 100K chars/file";
119
+ readonly description: "Optional full file content context.";
120
+ }];
121
+ readonly outputShape: "{summary, overallRisk, findings[], testsNeeded[], contextualInsights[], totalFindings}";
122
+ readonly gotchas: readonly ["Combined diff + file context is bounded by MAX_CONTEXT_CHARS.", "maxFindings caps output after generation."];
123
+ readonly crossToolFlow: readonly ["findings[].title -> suggest_search_replace.findingTitle", "findings[].explanation -> suggest_search_replace.findingDetails"];
124
+ readonly constraints: readonly ["Context budget (diff + files) < 500K chars."];
125
+ }, {
126
+ readonly name: "suggest_search_replace";
127
+ readonly purpose: "Generate verbatim search/replace fix blocks for one finding.";
128
+ readonly model: "gemini-2.5-pro";
129
+ readonly timeoutMs: 120000;
130
+ readonly thinkingBudget: 16384;
131
+ readonly maxOutputTokens: 4096;
132
+ readonly params: readonly [{
133
+ readonly name: "diff";
134
+ readonly type: "string";
135
+ readonly required: true;
136
+ readonly constraints: "10-120K chars";
137
+ readonly description: "Unified diff containing the target issue.";
138
+ }, {
139
+ readonly name: "findingTitle";
140
+ readonly type: "string";
141
+ readonly required: true;
142
+ readonly constraints: "3-160 chars";
143
+ readonly description: "Short finding title.";
144
+ }, {
145
+ readonly name: "findingDetails";
146
+ readonly type: "string";
147
+ readonly required: true;
148
+ readonly constraints: "10-3000 chars";
149
+ readonly description: "Detailed finding context.";
150
+ }];
151
+ readonly outputShape: "{summary, blocks[], validationChecklist[]}";
152
+ readonly gotchas: readonly ["One finding per call to avoid mixed patch intent.", "search must be exact whitespace-preserving match."];
153
+ readonly crossToolFlow: readonly ["Consumes findings from inspect_code_quality for targeted fixes."];
154
+ readonly constraints: readonly ["One finding per call; verbatim search match required."];
155
+ }, {
156
+ readonly name: "generate_test_plan";
157
+ readonly purpose: "Generate prioritized test cases and coverage guidance.";
158
+ readonly model: "gemini-2.5-flash";
159
+ readonly timeoutMs: 90000;
160
+ readonly thinkingBudget: 8192;
161
+ readonly maxOutputTokens: 4096;
162
+ readonly params: readonly [{
163
+ readonly name: "diff";
164
+ readonly type: "string";
165
+ readonly required: true;
166
+ readonly constraints: "10-120K chars";
167
+ readonly description: "Unified diff text.";
168
+ }, {
169
+ readonly name: "repository";
170
+ readonly type: "string";
171
+ readonly required: true;
172
+ readonly constraints: "1-200 chars";
173
+ readonly description: "Repository identifier (org/repo).";
174
+ }, {
175
+ readonly name: "language";
176
+ readonly type: "string";
177
+ readonly required: false;
178
+ readonly constraints: "2-32 chars";
179
+ readonly description: "Primary language hint.";
180
+ }, {
181
+ readonly name: "testFramework";
182
+ readonly type: "string";
183
+ readonly required: false;
184
+ readonly constraints: "1-50 chars";
185
+ readonly description: "Framework hint (jest, vitest, pytest, node:test).";
186
+ }, {
187
+ readonly name: "maxTestCases";
188
+ readonly type: "number";
189
+ readonly required: false;
190
+ readonly constraints: "1-30";
191
+ readonly description: "Post-generation cap applied to test cases.";
192
+ }];
193
+ readonly outputShape: "{summary, testCases[], coverageSummary}";
194
+ readonly gotchas: readonly ["maxTestCases caps output after generation."];
195
+ readonly crossToolFlow: readonly ["Pair with inspect_code_quality to validate high-risk paths."];
196
+ }, {
197
+ readonly name: "analyze_time_space_complexity";
198
+ readonly purpose: "Analyze Big-O complexity and detect degradations in changed code.";
199
+ readonly model: "gemini-2.5-flash";
200
+ readonly timeoutMs: 90000;
201
+ readonly thinkingBudget: 8192;
202
+ readonly maxOutputTokens: 2048;
203
+ readonly params: readonly [{
204
+ readonly name: "diff";
205
+ readonly type: "string";
206
+ readonly required: true;
207
+ readonly constraints: "10-120K chars";
208
+ readonly description: "Unified diff text.";
209
+ }, {
210
+ readonly name: "language";
211
+ readonly type: "string";
212
+ readonly required: false;
213
+ readonly constraints: "2-32 chars";
214
+ readonly description: "Primary language hint.";
215
+ }];
216
+ readonly outputShape: "{timeComplexity, spaceComplexity, explanation, potentialBottlenecks[], isDegradation}";
217
+ readonly gotchas: readonly ["Analyzes only changed code visible in the diff."];
218
+ readonly crossToolFlow: readonly ["Use for algorithmic/performance-sensitive changes."];
219
+ }, {
220
+ readonly name: "detect_api_breaking_changes";
221
+ readonly purpose: "Detect breaking API/interface changes in a diff.";
222
+ readonly model: "gemini-2.5-flash";
223
+ readonly timeoutMs: 90000;
224
+ readonly maxOutputTokens: 4096;
225
+ readonly params: readonly [{
226
+ readonly name: "diff";
227
+ readonly type: "string";
228
+ readonly required: true;
229
+ readonly constraints: "10-120K chars";
230
+ readonly description: "Unified diff text.";
231
+ }, {
232
+ readonly name: "language";
233
+ readonly type: "string";
234
+ readonly required: false;
235
+ readonly constraints: "2-32 chars";
236
+ readonly description: "Primary language hint.";
237
+ }];
238
+ readonly outputShape: "{hasBreakingChanges, breakingChanges[]}";
239
+ readonly gotchas: readonly ["Targets public API contracts over internal refactors."];
240
+ readonly crossToolFlow: readonly ["Run before merge for API-surface-sensitive changes."];
241
+ }];
242
+ export declare function getToolContracts(): readonly ToolContract[];
243
+ export declare function getToolContract(toolName: string): ToolContract | undefined;
244
+ export declare function requireToolContract(toolName: string): ToolContract;
245
+ export declare function getToolContractNames(): string[];