@j0hanz/code-review-analyst-mcp 1.2.0 → 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 +139 -68
  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 +7 -1
  18. package/dist/lib/tool-factory.js +85 -53
  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 +31 -26
  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 +27 -21
  45. package/dist/tools/generate-test-plan.js +35 -29
  46. package/dist/tools/index.js +9 -2
  47. package/dist/tools/inspect-code-quality.js +51 -41
  48. package/dist/tools/suggest-search-replace.js +34 -24
  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,14 +169,17 @@ 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
  }
127
178
  function getClient() {
128
- cachedClient ??= new GoogleGenAI({ apiKey: getApiKey() });
179
+ cachedClient ??= new GoogleGenAI({
180
+ apiKey: getApiKey(),
181
+ apiVersion: 'v1beta',
182
+ });
129
183
  return cachedClient;
130
184
  }
131
185
  export function setClientForTesting(client) {
@@ -180,7 +234,7 @@ function toNumericCode(candidate) {
180
234
  if (typeof candidate === 'number' && Number.isFinite(candidate)) {
181
235
  return candidate;
182
236
  }
183
- if (typeof candidate === 'string' && /^\d+$/.test(candidate)) {
237
+ if (typeof candidate === 'string' && DIGITS_ONLY_PATTERN.test(candidate)) {
184
238
  return Number.parseInt(candidate, 10);
185
239
  }
186
240
  return undefined;
@@ -192,43 +246,37 @@ function toUpperStringCode(candidate) {
192
246
  const normalized = candidate.trim().toUpperCase();
193
247
  return normalized.length > 0 ? normalized : undefined;
194
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
+ }
195
267
  function getNumericErrorCode(error) {
196
268
  const record = getNestedError(error);
197
269
  if (!record) {
198
270
  return undefined;
199
271
  }
200
- const fromStatus = toNumericCode(record.status);
201
- if (fromStatus !== undefined) {
202
- return fromStatus;
203
- }
204
- const fromStatusCode = toNumericCode(record.statusCode);
205
- if (fromStatusCode !== undefined) {
206
- return fromStatusCode;
207
- }
208
- const fromCode = toNumericCode(record.code);
209
- if (fromCode !== undefined) {
210
- return fromCode;
211
- }
212
- return undefined;
272
+ return findFirstNumericCode(record, ['status', 'statusCode', 'code']);
213
273
  }
214
274
  function getTransientErrorCode(error) {
215
275
  const record = getNestedError(error);
216
276
  if (!record) {
217
277
  return undefined;
218
278
  }
219
- const fromCode = toUpperStringCode(record.code);
220
- if (fromCode !== undefined) {
221
- return fromCode;
222
- }
223
- const fromStatus = toUpperStringCode(record.status);
224
- if (fromStatus !== undefined) {
225
- return fromStatus;
226
- }
227
- const fromStatusText = toUpperStringCode(record.statusText);
228
- if (fromStatusText !== undefined) {
229
- return fromStatusText;
230
- }
231
- return undefined;
279
+ return findFirstStringCode(record, ['code', 'status', 'statusText']);
232
280
  }
233
281
  function shouldRetry(error) {
234
282
  const numericCode = getNumericErrorCode(error);
@@ -251,21 +299,25 @@ function getRetryDelayMs(attempt) {
251
299
  return Math.min(RETRY_DELAY_MAX_MS, boundedDelay + jitter);
252
300
  }
253
301
  function buildGenerationConfig(request, abortSignal) {
254
- const systemInstruction = request.systemInstruction
255
- ? { systemInstruction: request.systemInstruction }
256
- : undefined;
257
- const thinkingConfig = getThinkingConfig(request.thinkingBudget);
258
- const safetySettings = getSafetySettings(getSafetyThreshold());
259
- return {
302
+ const includeThoughts = request.includeThoughts ?? getDefaultIncludeThoughts();
303
+ const thinkingConfig = getThinkingConfig(request.thinkingBudget, includeThoughts);
304
+ const config = {
260
305
  temperature: request.temperature ?? 0.2,
261
306
  maxOutputTokens: request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
262
307
  responseMimeType: 'application/json',
263
308
  responseSchema: request.responseSchema,
264
- ...(systemInstruction ?? undefined),
265
- ...(thinkingConfig ? { thinkingConfig } : undefined),
266
- safetySettings,
309
+ safetySettings: getSafetySettings(getSafetyThreshold()),
310
+ topP: 0.95,
311
+ topK: 40,
267
312
  abortSignal,
268
313
  };
314
+ if (request.systemInstruction) {
315
+ config.systemInstruction = request.systemInstruction;
316
+ }
317
+ if (thinkingConfig) {
318
+ config.thinkingConfig = thinkingConfig;
319
+ }
320
+ return config;
269
321
  }
270
322
  function combineSignals(signal, requestSignal) {
271
323
  return requestSignal ? AbortSignal.any([signal, requestSignal]) : signal;
@@ -281,6 +333,12 @@ function parseStructuredResponse(responseText) {
281
333
  throw new Error(`Model produced invalid JSON: ${getErrorMessage(error)}`);
282
334
  }
283
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
+ }
284
342
  async function generateContentWithTimeout(request, model, timeoutMs) {
285
343
  const controller = new AbortController();
286
344
  const timeout = setTimeout(() => {
@@ -300,7 +358,7 @@ async function generateContentWithTimeout(request, model, timeoutMs) {
300
358
  throw new Error('Gemini request was cancelled.');
301
359
  }
302
360
  if (controller.signal.aborted) {
303
- throw new Error(`Gemini request timed out after ${formatNumber(timeoutMs)}ms.`, { cause: error });
361
+ throw new Error(formatTimeoutErrorMessage(timeoutMs), { cause: error });
304
362
  }
305
363
  throw error;
306
364
  }
@@ -322,7 +380,7 @@ async function executeAttempt(request, model, timeoutMs, attempt, onLog) {
322
380
  });
323
381
  return parseStructuredResponse(response.text);
324
382
  }
325
- async function waitBeforeRetry(attempt, error, onLog) {
383
+ async function waitBeforeRetry(attempt, error, onLog, requestSignal) {
326
384
  const delayMs = getRetryDelayMs(attempt);
327
385
  const reason = getErrorMessage(error);
328
386
  await emitGeminiLog(onLog, 'warning', {
@@ -333,7 +391,20 @@ async function waitBeforeRetry(attempt, error, onLog) {
333
391
  reason,
334
392
  },
335
393
  });
336
- 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
+ }
337
408
  }
338
409
  async function throwGeminiFailure(maxRetries, lastError, onLog) {
339
410
  const attempts = maxRetries + 1;
@@ -355,27 +426,29 @@ async function runWithRetries(request, model, timeoutMs, maxRetries, onLog) {
355
426
  }
356
427
  catch (error) {
357
428
  lastError = error;
358
- if (attempt >= maxRetries || !shouldRetry(error)) {
429
+ if (!canRetryAttempt(attempt, maxRetries, error)) {
359
430
  break;
360
431
  }
361
- await waitBeforeRetry(attempt, error, onLog);
432
+ await waitBeforeRetry(attempt, error, onLog, request.signal);
362
433
  }
363
434
  }
364
435
  return throwGeminiFailure(maxRetries, lastError, onLog);
365
436
  }
437
+ function canRetryAttempt(attempt, maxRetries, error) {
438
+ return attempt < maxRetries && shouldRetry(error);
439
+ }
366
440
  async function waitForConcurrencySlot(limit, requestSignal) {
367
441
  const waitLimitMs = concurrencyWaitMsConfig.get();
368
442
  const pollMs = concurrencyPollMsConfig.get();
369
- const startedAt = performance.now();
443
+ const deadline = performance.now() + waitLimitMs;
370
444
  while (activeCalls >= limit) {
371
445
  if (requestSignal?.aborted) {
372
446
  throw new Error('Gemini request was cancelled.');
373
447
  }
374
- const elapsedMs = performance.now() - startedAt;
375
- if (elapsedMs >= waitLimitMs) {
376
- 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));
377
450
  }
378
- await sleep(pollMs, undefined, { ref: false });
451
+ await sleep(pollMs, undefined, SLEEP_UNREF_OPTIONS);
379
452
  }
380
453
  }
381
454
  export async function generateStructuredJson(request) {
@@ -387,9 +460,7 @@ export async function generateStructuredJson(request) {
387
460
  await waitForConcurrencySlot(limit, request.signal);
388
461
  activeCalls += 1;
389
462
  try {
390
- return await geminiContext.run({ requestId: nextRequestId(), model }, async () => {
391
- return runWithRetries(request, model, timeoutMs, maxRetries, onLog);
392
- });
463
+ return await geminiContext.run({ requestId: nextRequestId(), model }, () => runWithRetries(request, model, timeoutMs, maxRetries, onLog));
393
464
  }
394
465
  finally {
395
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[];